#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
    Blockstack
    ~~~~~
    copyright: (c) 2014-2015 by Halfmoon Labs, Inc.
    copyright: (c) 2016 by Blockstack.org

    This file is part of Blockstack

    Blockstack is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Blockstack is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    You should have received a copy of the GNU General Public License
    along with Blockstack. If not, see <http://www.gnu.org/licenses/>.
"""

from ..config import *
from ..hashing import *
from ..scripts import *
from ..nameset import *

from binascii import hexlify, unhexlify 

import blockstack_client
from blockstack_client.operations import *

import virtualchain
log = virtualchain.get_logger("blockstack-server")

# consensus hash fields (ORDER MATTERS!)
FIELDS = [
    'namespace_id',         # human-readable namespace ID
    'preorder_hash',        # hash(namespace_id,sender,reveal_addr) from the preorder (binds this namespace to its preorder)
    'version',              # namespace rules version

    'sender',               # the scriptPubKey hex script that identifies the preorderer
    'sender_pubkey',        # if sender is a p2pkh script, this is the public key
    'address',              # address of the sender, from the scriptPubKey
    'recipient',            # the scriptPubKey hex script that identifies the revealer.
    'recipient_address',    # the address of the revealer
    'block_number',         # block number at which this namespace was preordered
    'reveal_block',         # block number at which this namespace was revealed

    'op',                   # byte code identifying this operation to Blockstack
    'txid',                 # transaction ID at which this namespace was revealed
    'vtxindex',             # the index in the block where the tx occurs

    'lifetime',             # how long names last in this namespace (in number of blocks)
    'coeff',                # constant multiplicative coefficient on a name's price
    'base',                 # exponential base of a name's price
    'buckets',              # array that maps name length to the exponent to which to raise 'base' to
    'nonalpha_discount',    # multiplicative coefficient that drops a name's price if it has non-alpha characters 
    'no_vowel_discount',    # multiplicative coefficient that drops a name's price if it has no vowels
]

# fields this operation changes
# everything but the block number
MUTATE_FIELDS = filter( lambda f: f not in ["block_number"], FIELDS )

# fields that must be backed up when applying this operation (all of them)
BACKUP_FIELDS = ["__all__"]


def namespacereveal_sanity_check( namespace_id, version, lifetime, coeff, base, bucket_exponents, nonalpha_discount, no_vowel_discount ):
   """
   Verify the validity of a namespace reveal.
   Return True if valid
   Raise an Exception if not valid.
   """
   # sanity check 
   if not is_b40( namespace_id ) or "+" in namespace_id or namespace_id.count(".") > 0:
      raise Exception("Namespace ID '%s' has non-base-38 characters" % namespace_id)
   
   if len(namespace_id) > LENGTHS['blockchain_id_namespace_id']:
      raise Exception("Invalid namespace ID length for '%s' (expected length between 1 and %s)" % (namespace_id, LENGTHS['blockchain_id_namespace_id']))
   
   if lifetime < 0 or lifetime > (2**32 - 1):
      lifetime = NAMESPACE_LIFE_INFINITE 

   if coeff < 0 or coeff > 255:
      raise Exception("Invalid cost multiplier %s: must be in range [0, 256)" % coeff)
  
   if base < 0 or base > 255:
      raise Exception("Invalid base price %s: must be in range [0, 256)" % base)
 
   if type(bucket_exponents) != list:
        raise Exception("Bucket exponents must be a list")

   if len(bucket_exponents) != 16:
        raise Exception("Exactly 16 buckets required")

   for i in xrange(0, len(bucket_exponents)):
       if bucket_exponents[i] < 0 or bucket_exponents[i] > 15:
          raise Exception("Invalid bucket exponent %s (must be in range [0, 16)" % bucket_exponents[i])
   
   if nonalpha_discount <= 0 or nonalpha_discount > 15:
        raise Exception("Invalid non-alpha discount %s: must be in range [0, 16)" % nonalpha_discount)
    
   if no_vowel_discount <= 0 or no_vowel_discount > 15:
        raise Exception("Invalid no-vowel discount %s: must be in range [0, 16)" % no_vowel_discount)

   return True


@state_create( "namespace_id", "namespaces", "check_namespace_collision" )
def check( state_engine, nameop, block_id, checked_ops ):
    """
    Check a NAMESPACE_REVEAL operation to the name database.
    It is only valid if it is the first such operation
    for this namespace, and if it was sent by the same
    sender who sent the NAMESPACE_PREORDER.

    Return True if accepted
    Return False if not
    """

    namespace_id = nameop['namespace_id']
    namespace_id_hash = nameop['preorder_hash']
    sender = nameop['sender']
    namespace_preorder = None

    if not nameop.has_key('sender_pubkey'):
       log.debug("Namespace reveal requires a sender_pubkey (i.e. a p2pkh transaction)")
       return False

    if not nameop.has_key('recipient'):
       log.debug("No recipient script for namespace '%s'" % namespace_id)
       return False

    if not nameop.has_key('recipient_address'):
       log.debug("No recipient address for namespace '%s'" % namespace_id)
       return False

    # well-formed?
    if not is_b40( namespace_id ) or "+" in namespace_id or namespace_id.count(".") > 0:
       log.debug("Malformed namespace ID '%s': non-base-38 characters")
       return False

    # can't be revealed already
    if state_engine.is_namespace_revealed( namespace_id ):
       # this namespace was already revealed
       log.debug("Namespace '%s' is already revealed" % namespace_id )
       return False

    # can't be ready already
    if state_engine.is_namespace_ready( namespace_id ):
       # this namespace already exists (i.e. was already begun)
       log.debug("Namespace '%s' is already registered" % namespace_id )
       return False

    # must currently be preordered
    namespace_preorder = state_engine.get_namespace_preorder( namespace_id_hash )
    if namespace_preorder is None:
       # not preordered
       log.debug("Namespace '%s' is not preordered (no preorder %s)" % (namespace_id, namespace_id_hash) )
       return False

    # must be sent by the same principal who preordered it
    if namespace_preorder['sender'] != sender:
       # not sent by the preorderer
       log.debug("Namespace '%s' is not preordered by '%s'" % (namespace_id, sender))
       return False

    # must be a version we support
    if int(nameop['version']) != BLOCKSTACK_VERSION:
       log.debug("Namespace '%s' requires version %s, but this blockstack is version %s" % (namespace_id, nameop['version'], BLOCKSTACK_VERSION))
       return False

    # check fee...
    if not 'op_fee' in namespace_preorder:
       log.debug("Namespace '%s' preorder did not pay the fee" % (namespace_id))
       return False

    namespace_fee = namespace_preorder['op_fee']

    # must have paid enough
    if namespace_fee < price_namespace( namespace_id, block_id ):
       # not enough money
       log.debug("Namespace '%s' costs %s, but sender paid %s" % (namespace_id, price_namespace(namespace_id, block_id), namespace_fee ))
       return False

    # is this the first time this namespace has been revealed?
    old_namespace = state_engine.get_namespace_op_state( namespace_id, block_id, include_expired=True )
    namespace_block_number = None
    preorder_block_number = namespace_preorder['block_number']
    
    if old_namespace is None:
        # revealed for the first time
        log.debug("Revealing for the first time: '%s'" % namespace_id)
        namespace_block_number = namespace_preorder['block_number']
        state_create_put_prior_history( nameop, None )
        
    else:
        # revealed for the 2nd or later time
        log.debug("Re-revealing namespace '%s'" % namespace_id )
        
        # push back preorder block number to the original preorder
        namespace_block_number = old_namespace['block_number']

        # re-revealing
        prior_hist = prior_history_create( nameop, old_namespace, preorder_block_number, state_engine, extra_backup_fields=['consensus_hash','preorder_hash'])
        state_create_put_prior_history( nameop, prior_hist )

    # record preorder
    nameop['block_number'] = namespace_block_number  # namespace_preorder['block_number']
    nameop['reveal_block'] = block_id
    state_create_put_preorder( nameop, namespace_preorder )

    # NOTE: not fed into the consensus hash, but necessary for database constraints:
    nameop['ready_block'] = 0
    nameop['op_fee'] = namespace_preorder['op_fee']

    # can begin import
    return True


def get_reveal_recipient_from_outputs( outputs ):
    """
    There are between three outputs:
    * the OP_RETURN
    * the pay-to-address with the "reveal_addr", not the sender's address
    * the change address (i.e. from the namespace preorderer)
    
    Given the outputs from a namespace_reveal operation,
    find the revealer's address's script hex.
    
    By construction, it will be the first non-OP_RETURN 
    output (i.e. the second output).
    """
    
    ret = None
    if len(outputs) != 3:
        # invalid
        raise Exception("Outputs are not from a namespace reveal")

    reveal_output = outputs[1]
   
    output_script = reveal_output['scriptPubKey']
    output_asm = output_script.get('asm')
    output_hex = output_script.get('hex')
    output_addresses = output_script.get('addresses')
    
    if output_asm[0:9] != 'OP_RETURN' and output_hex is not None:
        
        # recipient's script hex
        ret = output_hex

    else:
       raise Exception("No namespace reveal script found")

    return ret


def tx_extract( payload, senders, inputs, outputs, block_id, vtxindex, txid ):
    """
    Extract and return a dict of fields from the underlying blockchain transaction data
    that are useful to this operation.
    """
  
    sender_script = None 
    sender_address = None 
    sender_pubkey_hex = None

    recipient_script = None 
    recipient_address = None 

    try:
       recipient_script = get_reveal_recipient_from_outputs( outputs )
       recipient_address = virtualchain.script_hex_to_address( recipient_script )

       assert recipient_script is not None 
       assert recipient_address is not None

       # by construction, the first input comes from the principal
       # who sent the reveal transaction...
       assert len(senders) > 0
       assert 'script_pubkey' in senders[0].keys()
       assert 'addresses' in senders[0].keys()

       sender_script = str(senders[0]['script_pubkey'])
       sender_address = str(senders[0]['addresses'][0])

       assert sender_script is not None 
       assert sender_address is not None

       if str(senders[0]['script_type']) == 'pubkeyhash':
          sender_pubkey_hex = get_public_key_hex_from_tx( inputs, sender_address )

    except Exception, e:
       log.exception(e)
       raise Exception("No reveal address")

    parsed_payload = parse( payload, sender_script, recipient_address )
    assert parsed_payload is not None 

    ret = {
       "sender": sender_script,
       "address": sender_address,
       "recipient": recipient_script,
       "recipient_address": recipient_address,
       "reveal_block": block_id,
       "vtxindex": vtxindex,
       "txid": txid,
       "op": NAMESPACE_REVEAL
    }

    ret.update( parsed_payload )

    if sender_pubkey_hex is not None:
        ret['sender_pubkey'] = sender_pubkey_hex
    else:
        ret['sender_pubkey'] = None

    return ret


def parse( bin_payload, sender_script, recipient_address ):
   """
   NOTE: the first three bytes will be missing
   """ 
   
   if len(bin_payload) < MIN_OP_LENGTHS['namespace_reveal']:
       raise AssertionError("Payload is too short to be a namespace reveal")

   off = 0
   life = None 
   coeff = None 
   base = None 
   bucket_hex = None
   buckets = []
   discount_hex = None
   nonalpha_discount = None 
   no_vowel_discount = None
   version = None
   namespace_id = None 
   namespace_id_hash = None
   
   life = int( hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_life']]), 16 )
   
   off += LENGTHS['blockchain_id_namespace_life']
   
   coeff = int( hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_coeff']]), 16 )
   
   off += LENGTHS['blockchain_id_namespace_coeff']
   
   base = int( hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_base']]), 16 )
   
   off += LENGTHS['blockchain_id_namespace_base']
   
   bucket_hex = hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_buckets']])
   
   off += LENGTHS['blockchain_id_namespace_buckets']
   
   discount_hex = hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_discounts']])
   
   off += LENGTHS['blockchain_id_namespace_discounts']
   
   version = int( hexlify(bin_payload[off:off+LENGTHS['blockchain_id_namespace_version']]), 16)
   
   off += LENGTHS['blockchain_id_namespace_version']
   
   namespace_id = bin_payload[off:]
   namespace_id_hash = None
   try:
       namespace_id_hash = hash_name( namespace_id, sender_script, register_addr=recipient_address )
   except:
       log.error("Invalid namespace ID and/or sender script")
       return None
   
   # extract buckets 
   buckets = [int(x, 16) for x in list(bucket_hex)]
   
   # extract discounts
   nonalpha_discount = int( list(discount_hex)[0], 16 )
   no_vowel_discount = int( list(discount_hex)[1], 16 )
  
   try:
       rc = namespacereveal_sanity_check( namespace_id, version, life, coeff, base, buckets, nonalpha_discount, no_vowel_discount )
       if not rc:
           raise Exception("Invalid namespace parameters")

   except Exception, e:
       log.error("Invalid namespace parameters")
       return None 

   return {
      'opcode': 'NAMESPACE_REVEAL',
      'lifetime': life,
      'coeff': coeff,
      'base': base,
      'buckets': buckets,
      'version': version,
      'nonalpha_discount': nonalpha_discount,
      'no_vowel_discount': no_vowel_discount,
      'namespace_id': namespace_id,
      'preorder_hash': namespace_id_hash
   }


def restore_delta( name_rec, block_number, history_index, working_db, untrusted_db ):
    """
    Find the fields in a name record that were changed by an instance of this operation, at the 
    given (block_number, history_index) point in time in the past.  The history_index is the
    index into the list of changes for this name record in the given block.

    Return the fields that were modified on success.
    Return None on error.
    """

    buckets = name_rec['buckets']

    if type(buckets) in [str, unicode]:
        # serialized bucket list.
        # unserialize 
        reg = "[" + "[ ]*[0-9]+[ ]*," * 15 + "[ ]*[0-9]+[ ]*]"
        match = re.match( reg, buckets )
        if match is None:
            log.error("FATAL: bucket list '%s' is not parsable" % (buckets))
            os.abort()

        try:
            buckets = [int(b) for b in buckets.strip("[]").split(", ")]
        except Exception, e:
            log.exception(e)
            log.error("FATAL: failed to parse '%s' into a 16-elemenet list" % (buckets))
            os.abort()

    name_rec_script = build_namespace_reveal( str(name_rec['namespace_id']), name_rec['version'], str(name_rec['recipient_address']), \
                                              name_rec['lifetime'], name_rec['coeff'], name_rec['base'], buckets, 
                                              name_rec['nonalpha_discount'], name_rec['no_vowel_discount'] )

    name_rec_payload = unhexlify( name_rec_script )[3:]
    ret_op = parse( name_rec_payload, str(name_rec['sender']), str(name_rec['recipient_address']) )

    ret_op['op'] = NAMESPACE_REVEAL

    return ret_op


def snv_consensus_extras( name_rec, block_id, blockchain_name_data, db ):
    """
    Calculate any derived missing data that goes into the check() operation,
    given the block number, the name record at the block number, and the db.
    """
    return blockstack_client.operations.namespacereveal.snv_consensus_extras( name_rec, block_id, blockchain_name_data )
    '''
    return {}
    '''
