package snowblossom.node;
import com.google.common.collect.TreeMultimap;
import com.google.protobuf.ByteString;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import snowblossom.lib.*;
import snowblossom.lib.db.DB;
import snowblossom.lib.db.DBMapMutationSet;
import snowblossom.lib.trie.HashedTrie;
import snowblossom.proto.*;
import snowblossom.trie.proto.TrieNode;
import java.text.Collator;
import java.text.Normalizer;
import java.util.Locale;

public class ForBenefitOfUtil
{
  public static final ByteString FBO_MAP_PREFIX = ByteString.copyFrom("fbo2".getBytes());
  public static final ByteString ID_MAP_PREFIX = ByteString.copyFrom("id2o".getBytes());
  public static final ByteString ID_MAP_USER = ByteString.copyFrom("user".getBytes());
  public static final ByteString ID_MAP_CHAN = ByteString.copyFrom("chan".getBytes());


  public static void saveIndex(Block blk, Map<ByteString, ByteString> update_map)
  {
    int height = blk.getHeader().getBlockHeight();

    ChainHash blk_id = new ChainHash(blk.getHeader().getSnowHash());

    for(Transaction tx : blk.getTransactionsList())
    {
      TransactionInner inner = TransactionUtil.getInner(tx);

      int out_idx = 0;
      for(TransactionOutput out : inner.getOutputsList())
      {
        ChainHash tx_id = new ChainHash(tx.getTxHash());
        if (out.getForBenefitOfSpecHash().size() == Globals.ADDRESS_SPEC_HASH_LEN)
        {
          ByteString for_addr = out.getForBenefitOfSpecHash();

          ByteString key = getKey(for_addr, tx_id, out_idx);

          update_map.put(key, out.toByteString());
        }
        if (out.getIds().getUsername().size() > 0)
        {
          ByteString key = getIdKey(ID_MAP_USER, out.getIds().getUsername(), height, tx_id, out_idx);
          update_map.put(key, out.toByteString());
        }
        if (out.getIds().getChannelname().size() > 0)
        {
          ByteString key = getIdKey(ID_MAP_CHAN, out.getIds().getChannelname(), height, tx_id, out_idx);
          update_map.put(key, out.toByteString());
        }
        out_idx++;
      }
    }
  }

  public static ByteString getKey(ByteString for_addr, ChainHash tx_id, int out_idx)
  {
    byte[] buff = new byte[Globals.ADDRESS_SPEC_HASH_LEN + Globals.BLOCKCHAIN_HASH_LEN + 4];

    ByteBuffer bb = ByteBuffer.wrap(buff);
    bb.put(for_addr.toByteArray());
    bb.put(tx_id.toByteArray());
    bb.putInt(out_idx);

    return FBO_MAP_PREFIX.concat(ByteString.copyFrom(buff));
  }

  public static ByteString getIdKey(ByteString type, ByteString name, int height, ChainHash tx_id, int out_idx)
  {
    ByteString name_hash = normalizeAndHash(name);

    return ID_MAP_PREFIX
      .concat(type)
      .concat(name_hash)
      .concat(getIntString(height))
      .concat(tx_id.getBytes())
      .concat(getIntString(out_idx));
  }


  public static ByteString getIntString(int x)
  {
    byte[] buff = new byte[4];
    ByteBuffer bb = ByteBuffer.wrap(buff);
    bb.putInt(x);
    ByteString str = ByteString.copyFrom(buff);
    return str;
  }


  public static ByteString getValue(ChainHash tx_id, int out_idx)
  {
    byte[] buff = new byte[Globals.BLOCKCHAIN_HASH_LEN + 4];

    ByteBuffer bb = ByteBuffer.wrap(buff);
    bb.put(tx_id.toByteArray());
    bb.putInt(out_idx);

    return ByteString.copyFrom(buff);
  }

  protected static TxOutPoint getOutpoint(ByteString tx_info, ByteString val)
  {
    if (tx_info.size() != Globals.BLOCKCHAIN_HASH_LEN + 4)
    {
      throw new RuntimeException("Wrong size tx_info: " + tx_info.size());
    }
    
    TxOutPoint.Builder op = TxOutPoint.newBuilder();

    op.setTxHash( tx_info.substring(0, Globals.BLOCKCHAIN_HASH_LEN));
    op.setOutIdx( tx_info.substring(Globals.BLOCKCHAIN_HASH_LEN).asReadOnlyByteBuffer().getInt() );

    try
    {
      op.setOut( TransactionOutput.parseFrom(val) );
    }
    catch(com.google.protobuf.InvalidProtocolBufferException e)
    {
      throw new RuntimeException(e);
    }

    return op.build();
  }

  /**
   * Return a list of outputs that are marked in favor of 'spec_hash'
   */
  public static TxOutList getFBOList(AddressSpecHash spec_hash, DB node_db, BlockSummary head)
  {
    HashedTrie db = node_db.getChainIndexTrie();
    ByteString trie_root = head.getChainIndexTrieHash();
    // TODO - filter by currently valid utxos
    TxOutList.Builder list = TxOutList.newBuilder();

    ByteString search_key = FBO_MAP_PREFIX.concat(spec_hash.getBytes()); 
    
    TreeMap<ByteString, ByteString> map = db.getDataMap(trie_root, search_key, 10000);

    for(Map.Entry<ByteString, ByteString> me : map.entrySet())
    {
      ByteString tx_info = me.getKey().substring( search_key.size() );

      list.addOutList( getOutpoint(tx_info, me.getValue()));
    }
    return filterByCurrent(list.build(), node_db, head);
  }

  /**
   * returns a list of outputs that are marked with the name 'name' in ascending
   * order of confirmation height.  So oldest first.
   */
  public static TxOutList getIdList(ByteString type, ByteString name, DB node_db, BlockSummary head)
  {
    HashedTrie db = node_db.getChainIndexTrie();
    ByteString trie_root = head.getChainIndexTrieHash();
    // TODO - filter by currently valid utxos
    ByteString name_hash = normalizeAndHash(name);

    TxOutList.Builder list = TxOutList.newBuilder();
    ByteString search_key = ID_MAP_PREFIX
      .concat(type)
      .concat(name_hash);

    TreeMap<ByteString, ByteString> map = db.getDataMap(trie_root, search_key, 10000);

    for(Map.Entry<ByteString, ByteString> me : map.entrySet())
    {
      ByteString tx_info = me.getKey().substring( search_key.size() + 4 );

      list.addOutList( getOutpoint(tx_info, me.getValue()));
    }

    return filterByCurrent(list.build(), node_db, head);
  }


  /**
   * Filter the list of OutPoints, returning only those that are still
   * in the 'head' utxo set
   */
  public static TxOutList filterByCurrent(TxOutList input, DB node_db, BlockSummary head)
  {
    ByteString utxo_root_hash = head.getHeader().getUtxoRootHash();

    UtxoUpdateBuffer utxo_buff = new UtxoUpdateBuffer( node_db.getUtxoHashedTrie(), new ChainHash(utxo_root_hash));

    TxOutList.Builder list = TxOutList.newBuilder();
    for(TxOutPoint out : input.getOutListList())
    {
      if (utxo_buff.checkOutpointExists(out))
      {
        list.addOutList( out );
      }
    }

    return list.build();
  }

  public static TxOutList getIdListUser(ByteString name, DB node_db, BlockSummary head)
  {
    return getIdList(ID_MAP_USER, name, node_db, head);
  }

  public static TxOutList getIdListChannel(ByteString name, DB node_db, BlockSummary head)
  {
    return getIdList(ID_MAP_CHAN, name, node_db, head);
  }

  /**
   * This doesn't output the string, but actually the collator byte stream
   * which should be used as the uniqueness key. See ForBenefitOfUtilTest for examples
   * of things that should or should not match each other.
   */
  public static ByteString normalize(String input)
  {
    String n1 = Normalizer.normalize(input, Normalizer.Form.NFC);

    // Not trying to be US-English centric, but have to pick some Locale
    // so that this section will behave consistently for all nodes
    Collator collator = Collator.getInstance(Locale.US);
    collator.setStrength(Collator.PRIMARY);
    collator.setDecomposition(Collator.FULL_DECOMPOSITION);
    
    return ByteString.copyFrom(collator.getCollationKey(n1).toByteArray());

  }
  public static ByteString normalizeAndHash(ByteString name)
  {
    ByteString str = normalize(new String(name.toByteArray()));

    return DigestUtil.hash(str);
    
  }

}