#!/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/>.
"""

import preorder
import register
import transfer
import update
import revoke
import nameimport
import namespacepreorder
import namespacereveal
import namespaceready
import announce

import binascii
import copy

from ..nameset import CONSENSUS_FIELDS_REQUIRED, NAMEREC_MUTATE_FIELDS, NAMEREC_BACKUP_FIELDS
from ..config import *

from .register import get_registration_recipient_from_outputs 
from .transfer import get_transfer_recipient_from_outputs
from .nameimport import get_import_update_hash_from_outputs

from .preorder import tx_extract as extract_preorder, \
    restore_delta as restore_preorder, \
    check as check_preorder, snv_consensus_extras as preorder_consensus_extras
from .register import tx_extract as extract_registration, \
    restore_delta as restore_register, \
    snv_consensus_extras as register_consensus_extras, check_register as check_registration, check_renewal
from .transfer import tx_extract as extract_transfer, \
    restore_delta as restore_transfer, \
    snv_consensus_extras as transfer_consensus_extras, check as check_transfer
from .update import tx_extract as extract_update, \
    restore_delta as restore_update, \
    snv_consensus_extras as update_consensus_extras, check as check_update
from .revoke import tx_extract as extract_revoke, \
    restore_delta as restore_revoke, \
    check as check_revoke, snv_consensus_extras as revoke_consensus_extras
from .namespacepreorder import tx_extract as extract_namespace_preorder, \
    restore_delta as restore_namespace_preorder, \
    check as check_namespace_preorder, snv_consensus_extras as namespace_preorder_consensus_extras
from .nameimport import tx_extract as extract_name_import, \
    restore_delta as restore_name_import, \
    snv_consensus_extras as name_import_consensus_extras, check as check_name_import
from .namespacereveal import tx_extract as extract_namespace_reveal, \
    restore_delta as restore_namespace_reveal, \
    check as check_namespace_reveal, snv_consensus_extras as namespace_reveal_consensus_extras
from .namespaceready import tx_extract as extract_namespace_ready, \
    restore_delta as restore_namespace_ready, \
    check as check_namespace_ready, snv_consensus_extras as namespace_ready_consensus_extras
from .announce import tx_extract as extract_announce, \
    restore_delta as restore_announce, \
    check as check_announce, snv_consensus_extras as announce_consensus_extras

SERIALIZE_FIELDS = {
    "NAME_PREORDER": preorder.FIELDS,
    "NAME_REGISTRATION": register.FIELDS,
    "NAME_RENEWAL": register.FIELDS,
    "NAME_UPDATE": update.FIELDS,
    "NAME_TRANSFER": transfer.FIELDS,
    "NAME_REVOKE": revoke.FIELDS,
    "NAME_IMPORT": nameimport.FIELDS,
    "NAMESPACE_PREORDER": namespacepreorder.FIELDS,
    "NAMESPACE_REVEAL": namespacereveal.FIELDS,
    "NAMESPACE_READY": namespaceready.FIELDS,
    "ANNOUNCE": announce.FIELDS
}

MUTATE_FIELDS = {
    "NAME_PREORDER": preorder.MUTATE_FIELDS,
    "NAME_REGISTRATION": register.REGISTER_MUTATE_FIELDS,
    "NAME_RENEWAL": register.RENEWAL_MUTATE_FIELDS,
    "NAME_UPDATE": update.MUTATE_FIELDS,
    "NAME_TRANSFER": transfer.MUTATE_FIELDS,
    "NAME_REVOKE": revoke.MUTATE_FIELDS,
    "NAME_IMPORT": nameimport.MUTATE_FIELDS,
    "NAMESPACE_PREORDER": namespacepreorder.MUTATE_FIELDS,
    "NAMESPACE_REVEAL": namespacereveal.MUTATE_FIELDS,
    "NAMESPACE_READY": namespaceready.MUTATE_FIELDS,
    "ANNOUNCE": announce.MUTATE_FIELDS
}

BACKUP_FIELDS = {
    "NAME_PREORDER": preorder.BACKUP_FIELDS,
    "NAME_REGISTRATION": register.REGISTER_BACKUP_FIELDS,
    "NAME_RENEWAL": register.RENEWAL_BACKUP_FIELDS,
    "NAME_UPDATE": update.BACKUP_FIELDS,
    "NAME_TRANSFER": transfer.BACKUP_FIELDS,
    "NAME_REVOKE": revoke.BACKUP_FIELDS,
    "NAME_IMPORT": nameimport.BACKUP_FIELDS,
    "NAMESPACE_PREORDER": namespacepreorder.BACKUP_FIELDS,
    "NAMESPACE_REVEAL": namespacereveal.BACKUP_FIELDS,
    "NAMESPACE_READY": namespaceready.BACKUP_FIELDS,
    "ANNOUNCE": announce.BACKUP_FIELDS
}

# NOTE: these all have the same signatures
EXTRACT_METHODS = {
    "NAME_PREORDER": extract_preorder,
    "NAME_REGISTRATION": extract_registration,
    "NAME_RENEWAL": extract_registration,
    "NAME_UPDATE": extract_update,
    "NAME_TRANSFER": extract_transfer,
    "NAME_REVOKE": extract_revoke,
    "NAME_IMPORT": extract_name_import,
    "NAMESPACE_PREORDER": extract_namespace_preorder,
    "NAMESPACE_REVEAL": extract_namespace_reveal,
    "NAMESPACE_READY": extract_namespace_ready,
    "ANNOUNCE": extract_announce
}

# NOTE: these all have the same signature
CHECK_METHODS = {
    "NAME_PREORDER": check_preorder,
    "NAME_REGISTRATION": check_registration,
    "NAME_RENEWAL": check_renewal,
    "NAME_UPDATE": check_update,
    "NAME_TRANSFER": check_transfer,
    "NAME_REVOKE": check_revoke,
    "NAME_IMPORT": check_name_import,
    "NAMESPACE_PREORDER": check_namespace_preorder,
    "NAMESPACE_REVEAL": check_namespace_reveal,
    "NAMESPACE_READY": check_namespace_ready,
    "ANNOUNCE": check_announce
}


# NOTE: these all have the same signatures 
RESTORE_METHODS = {
    "NAME_PREORDER": restore_preorder,
    "NAME_REGISTRATION": restore_register,
    "NAME_RENEWAL": restore_register,
    "NAME_UPDATE": restore_update,
    "NAME_TRANSFER": restore_transfer,
    "NAME_REVOKE": restore_revoke,
    "NAME_IMPORT": restore_name_import,
    "NAMESPACE_PREORDER": restore_namespace_preorder,
    "NAMESPACE_REVEAL": restore_namespace_reveal,
    "NAMESPACE_READY": restore_namespace_ready,
    "ANNOUNCE": restore_announce 
}


# NOTE: these all have the same signatures 
SNV_CONSENSUS_EXTRA_METHODS = {
     "NAME_PREORDER": preorder_consensus_extras,
     "NAME_REGISTRATION": register_consensus_extras,
     "NAME_RENEWAL": register_consensus_extras,
     "NAME_UPDATE": update_consensus_extras,
     "NAME_TRANSFER": transfer_consensus_extras,
     "NAME_REVOKE": revoke_consensus_extras,
     "NAME_IMPORT": name_import_consensus_extras,
     "NAMESPACE_PREORDER": namespace_preorder_consensus_extras,
     "NAMESPACE_REVEAL": namespace_reveal_consensus_extras,
     "NAMESPACE_READY": namespace_ready_consensus_extras,
     "ANNOUNCE": announce_consensus_extras
}


# build-in sanity checks....
# required consensus fields are required!
for opcode, serialize_set in SERIALIZE_FIELDS.items():
    if len(serialize_set) == 0:
        continue

    for required_consensus_field in CONSENSUS_FIELDS_REQUIRED:
        if required_consensus_field not in serialize_set:
            # do not even allow this package to be imported 
            raise Exception("BUG: missing required consensus field '%s' in '%s' definition" % (required_consensus_field, opcode))

# required mutate fields must be present 
for opcode, mutate_set in MUTATE_FIELDS.items():
    if len(mutate_set) == 0:
        continue 

    for required_mutate_field in NAMEREC_MUTATE_FIELDS:
        if required_mutate_field not in mutate_set:
            # do not even allow this package to be imported 
            raise Exception("BUG: missing required mutate field '%s' of '%s' definition" % (required_mutate_field, opcode))


# required backup fields must be present 
for opcode, backup_set in BACKUP_FIELDS.items():
    if len(backup_set) == 0:
        continue

    if '__all__' in backup_set:
        # everything will be backed up
        continue

    for required_backup_field in NAMEREC_BACKUP_FIELDS:
        if required_backup_field not in backup_set:
            # do not even allow this package to be imported 
            raise Exception("BUG: missing required backup field '%s' of '%s' definition" % (required_backup_field, opcode))


# mutate fields must be a subset of backup fields 
for opcode, mutate_set in MUTATE_FIELDS.items():
    for mutate_field in mutate_set:

        if '__all__' in BACKUP_FIELDS[opcode]:
            # everything will be backed up
            continue 

        if mutate_field not in BACKUP_FIELDS[opcode]:
            # do not even allow this package to be imported 
            raise Exception("BUG: mutate field '%s' is not present in the backup fields for '%s'" % (mutate_field, opcode))

del opcode
del mutate_set
del backup_set
del serialize_set
del mutate_field
del required_backup_field
del required_mutate_field
del required_consensus_field


def op_extract( op_name, data, senders, inputs, outputs, block_id, vtxindex, txid ):
    """
    Extract an operation from transaction data.
    Return the extracted fields as a dict.
    """

    global EXTRACT_METHODS

    if op_name not in EXTRACT_METHODS.keys():
        raise Exception("No such operation '%s'" % op_name)

    method = EXTRACT_METHODS[op_name]
    op_data = method( data, senders, inputs, outputs, block_id, vtxindex, txid )
    return op_data


def op_check_quirks( state_engine, nameop, block_id, checked_ops ):
    """
    Given the set of arguments for op_check, apply any 
    op-specific quirks that are needed to preserve backwards compatibility
    """
    if nameop['opcode'] == 'NAME_IMPORT':
        nameop['op_fee'] = float(nameop['op_fee'])


def op_snv_consensus_extra_quirks( extras, namerec, block_id, commit, db ):
    """
    Given the set of arguments to snv_consensus_extras, apply any
    op-specific quirks that are needed to preserve backwards compatibility
    """
    return blockstack_client.operations.nameop_snv_consensus_extra_quirks( extras, namerec, block_id )
   

def op_make_restore_diff_quirks( diff, op_name, cur_rec, prev_block_number, history_index, untrusted_db ):
    """
    Given the set of arguments to restore_diff, apply any op-specific quirks
    that are needed to preserve backwards compatibility
    """
    last_creation_op = cur_rec.get('last_creation_op', None)
    last_creation_opcode = None

    if last_creation_op is not None:
        last_creation_opcode = OPCODE_NAMES.get(last_creation_op, None)

    if last_creation_opcode is None:
        if cur_rec['op'] == NAME_IMPORT:
            # this is the first-ever import
            last_creation_opcode = 'NAME_IMPORT'

        elif cur_rec['op'] == NAME_PREORDER:
            # this is the first-ever preorder
            last_creation_opcode = 'NAME_PREORDER'

    log.debug("apply RESTORE DIFF QUIRKS on %s at %s[%s] (created with %s)" % (cur_rec.get('name', "UNKNOWN"), prev_block_number, history_index, last_creation_opcode))

    if cur_rec.has_key('name') and last_creation_opcode == 'NAME_IMPORT':
        log.debug("apply RESTORE DIFF QUIRK on %s: %s --> %s"  % (cur_rec.get('name', "UNKNOWN"), cur_rec['op_fee'], float(cur_rec['op_fee'])))
        diff['op_fee'] = float(cur_rec['op_fee'])


def op_check( state_engine, nameop, block_id, checked_ops ):
    """
    Given the state engine, the current block, the list of pending
    operations processed so far, and the current operation, determine
    whether or not it should be accepted.

    The operation is allowed to change once, as a result of a check
    """

    global CHECK_METHODS, MUTATE_FIELDS

    count = 0
    while count < 3:

        count += 1

        nameop_clone = copy.deepcopy( nameop )
        opcode = None

        if 'opcode' not in nameop_clone.keys():
            op = nameop_clone.get('op', None)
            try:
                assert op is not None, "BUG: no op defined"
                opcode = op_get_opcode_name( op )
                assert opcode is not None, "BUG: op '%s' undefined" % op
            except Exception, e:
                log.exception(e)
                log.error("FATAL: BUG: no 'op' defined")
                sys.exit(1)

        else:
            opcode = nameop_clone['opcode']
  
        check_method = CHECK_METHODS.get( opcode, None )
        try:
            assert check_method is not None, "BUG: no check-method for '%s'" % opcode
        except Exception, e:
            log.exception(e)
            log.error("FATAL: BUG: no check-method for '%s'" % opcode )
            sys.exit(1)

        rc = check_method( state_engine, nameop_clone, block_id, checked_ops )
        if not rc:
            # rejected
            break

        # did the opcode change?
        # i.e. did the nameop get transformed into a different opcode?
        new_opcode = nameop_clone.get( 'opcode', None )
        if new_opcode is None or new_opcode == opcode:
            # we're done
            nameop.clear()
            nameop.update( nameop_clone )
            break

        else:
            # try again 
            log.debug("Nameop re-interpreted from '%s' to '%s' (%s)" % (opcode, new_opcode, count))
            nameop['opcode'] = new_opcode 
            continue

    try:
        assert count < 3, "opcode flipflop loop detected"
    except Exception, e:
        log.exception(e)
        log.error("FATAL: BUG: flipflop loop")
        sys.exit(1)

    if rc:
        op_check_quirks( state_engine, nameop, block_id, checked_ops )

    return rc


def op_make_restore_diff( op_name, cur_rec, prev_block_number, history_index, working_db, untrusted_db ):
    """
    Given a current name record, an operation name, and a (block number, block history index) coordinate,
    calculate a diff that, when applied to the given name record, will restore it to the name
    record as it was when the operation at (block number, block history index) was applied.
    """

    global RESTORE_METHODS, MUTATE_FIELDS

    if op_name not in RESTORE_METHODS.keys():
        raise Exception("No such operation '%s'" % op_name)

    method = RESTORE_METHODS[op_name]
    delta = method( cur_rec, prev_block_number, history_index, working_db, untrusted_db )
    op_make_restore_diff_quirks( delta, op_name, cur_rec, prev_block_number, history_index, untrusted_db )
    return delta 


def op_get_mutate_fields( op_name ):
    """
    Get the names of the fields that will change
    when this operation gets applied to a record.
    """

    global MUTATE_FIELDS

    if op_name not in MUTATE_FIELDS.keys():
        raise Exception("No such operation '%s'" % op_name)

    fields = MUTATE_FIELDS[op_name][:]
    return fields


def op_get_backup_fields( op_name ):
    """
    Get the set of fields to back up to a name's history
    when applying this operation.
    These fields should encompass sufficient
    information to calculate a diff that will restore
    a future version of a name record to the state it is in now.
    (NOTE this is different from the mutate fields--
    some operations need to back up fields even though
    they wont be changed, since the consensus hash
    is derived from them.)
    """

    global BACKUP_FIELDS 

    if op_name not in BACKUP_FIELDS.keys():
        raise Exception("No such operation '%s'" % op_name )

    fields = BACKUP_FIELDS[op_name][:]
    return fields


def op_get_consensus_fields( op_name ):
    """
    Get the set of consensus-generating fields for an operation.
    """

    global SERIALIZE_FIELDS
    
    if op_name not in SERIALIZE_FIELDS.keys():
        raise Exception("No such operation '%s'" % op_name )

    fields = SERIALIZE_FIELDS[op_name][:]
    return fields


def op_snv_consensus_extra( op_name, prev_name_rec, prev_block_id, db ):
    """
    Derive any missing consensus-generating fields from the 
    fields of a name record (since some of them
    are dynamically generated when the operation
    is discovered).  This method is used for
    calculating prior operations from name records
    for SNV.

    The given name record is the name record as it was when
    prev_block_id was processed.  The 'vtxindex' field within
    the name record indicates which the transaction at which
    it existed.  I.e., the given name record is in the state
    it was in at (prev_block_id, prev_name_rec['vtxindex']).

    Return the extra conesnsus fields on success.
    Return None on error.
    """
    global SNV_CONSENSUS_EXTRA_METHODS 

    if op_name not in SNV_CONSENSUS_EXTRA_METHODS.keys():
        raise Exception("No such operation '%s'" % op_name)

    method = SNV_CONSENSUS_EXTRA_METHODS[op_name]
    extras = method( prev_name_rec, prev_block_id, None, db )
    extras = blockstack_client.operations.nameop_snv_consensus_extra_quirks( extras, prev_name_rec, prev_block_id )
    # op_snv_consensus_extra_quirks( extras, prev_name_rec, prev_block_id, False, db )
    return extras 


def op_commit_consensus_extra( op_name, committed_name_rec, blockchain_name_data, block_id, db ):
    """
    Like op_snv_consensus_extra, but will be called with the
    current name record and block number, in order to re-calculate
    any derived consensus-affecting fields.
    """

    global SNV_CONSENSUS_EXTRA_METHODS, SERIALIZE_FIELDS
 
    if op_name not in SNV_CONSENSUS_EXTRA_METHODS.keys():
        raise Exception("No such operation '%s'" % op_name)

    if op_name not in SERIALIZE_FIELDS.keys():
        raise Exception("No such operation '%s'" % op_name)

    method = SNV_CONSENSUS_EXTRA_METHODS[op_name]
    commit_fields = SERIALIZE_FIELDS[op_name]

    extras = method( committed_name_rec, block_id, blockchain_name_data, db )
    extras = blockstack_client.operations.nameop_snv_consensus_extra_quirks( extras, committed_name_rec, block_id )
    # extras = op_snv_consensus_extra_quirks( extras, committed_name_rec, block_id, True, db )

    commit_extras = {}
    for cf in commit_fields + ['__override__']:
        if cf in extras:
            commit_extras[cf] = extras[cf]

    return commit_extras


def op_commit_consensus_override( consensus_extras, field ):
    """
    Force a consensus field to change on commit.
    This is used in the event that an operation encodes one value
    for this field, but we need to mix a different value for the 
    field into the operation we actually commit.

    This is used to stay compatible with bugs in previous implementations.
    """
    if not consensus_extras.has_key( '__override__' ):
        consensus_extras['__override__'] = [field]
    else:
        consensus_extras['__override__'].append( field )


def op_commit_consensus_has_override( consensus_extras, field ):
    """
    Is a consensus field overridden?
    """
    if consensus_extras.has_key( '__override__' ):
        if field in consensus_extras['__override__']:
            return True

    return False

def op_commit_consensus_sanitize( consensus_extras ):
    """
    Remove any non-commit metadata fields
    """
    for k in ['__override__']:
        if k in consensus_extras.keys():
            del consensus_extras[k]

    return consensus_extras


def op_commit_consensus_get_overrides( consensus_extras ):
    """
    get overridden field names
    """
    return consensus_extras.get("__override__", [])


