/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.usergrid.persistence.collection.serialization.impl;


import java.nio.ByteBuffer;
import java.util.*;

import com.datastax.driver.core.DataType;
import com.datastax.driver.core.ProtocolVersion;
import com.datastax.driver.core.Session;

import org.apache.usergrid.persistence.collection.serialization.SerializationFig;
import org.apache.usergrid.persistence.core.CassandraConfig;
import org.apache.usergrid.persistence.core.CassandraFig;
import org.apache.usergrid.persistence.core.astyanax.MultiTenantColumnFamilyDefinition;
import org.apache.usergrid.persistence.core.datastax.CQLUtils;
import org.apache.usergrid.persistence.core.datastax.TableDefinition;
import org.apache.usergrid.persistence.core.datastax.impl.TableDefinitionImpl;
import org.apache.usergrid.persistence.model.entity.Id;

import com.google.inject.Inject;
import com.google.inject.Singleton;


/**
 * V1 impl with unique value serialization strategy with the collection scope
 */
@Singleton
public class UniqueValueSerializationStrategyV2Impl  extends UniqueValueSerializationStrategyImpl<TypeField, Id> {

    private static final String UNIQUE_VALUES_TABLE = CQLUtils.quote("Unique_Values_V2");
    private static final Collection<String> UNIQUE_VALUES_PARTITION_KEYS = Collections.singletonList("key");
    private static final Collection<String> UNIQUE_VALUES_COLUMN_KEYS = Collections.singletonList("column1");
    private static final Map<String, DataType.Name> UNIQUE_VALUES_COLUMNS =
        new HashMap<String, DataType.Name>() {{
            put( "key", DataType.Name.BLOB );
            put( "column1", DataType.Name.CUSTOM );
            put( "value", DataType.Name.BLOB ); }};
    private static final Map<String, String> UNIQUE_VALUES_CLUSTERING_ORDER =
        new HashMap<String, String>(){{ put( "column1", "ASC" );}};


    private static final String UNIQUE_VALUES_LOG_TABLE = CQLUtils.quote("Entity_Unique_Values_V2");
    private static final Collection<String> UNIQUE_VALUES_LOG_PARTITION_KEYS = Collections.singletonList("key");
    private static final Collection<String> UNIQUE_VALUES_LOG_COLUMN_KEYS = Collections.singletonList("column1");
    private static final Map<String, DataType.Name> UNIQUE_VALUES_LOG_COLUMNS =
        new HashMap<String, DataType.Name>() {{
            put( "key", DataType.Name.BLOB );
            put( "column1", DataType.Name.CUSTOM );
            put( "value", DataType.Name.BLOB ); }};
    private static final Map<String, String> UNIQUE_VALUES_LOG_CLUSTERING_ORDER =
        new HashMap<String, String>(){{ put( "column1", "ASC" );}};

    private static TableDefinition uniqueValues;

    private static TableDefinition uniqueValuesLog;


    /**
     * Construct serialization strategy for keyspace.
     *
     * @param cassandraFig The cassandra configuration
     * @param serializationFig The serialization configuration
     *
     */
    @Inject
    public UniqueValueSerializationStrategyV2Impl( final CassandraFig cassandraFig,
                                                   final SerializationFig serializationFig,
                                                   final Session session,
                                                   final CassandraConfig cassandraConfig) {
        super( cassandraFig, serializationFig, session, cassandraConfig );
    }


    @Override
    public Collection<MultiTenantColumnFamilyDefinition> getColumnFamilies() {

        return Collections.emptyList();

    }

    @Override
    public Collection<TableDefinition> getTables() {

        final TableDefinition uniqueValues = getUniqueValuesTable( cassandraFig );
        final TableDefinition uniqueValuesLog = getEntityUniqueLogTable( cassandraFig );

        return Arrays.asList( uniqueValues, uniqueValuesLog );

    }


    @Override
    protected TableDefinition getUniqueValuesTable( CassandraFig cassandraFig ) {
        if ( uniqueValues == null ) {
            uniqueValues = new TableDefinitionImpl( cassandraFig.getApplicationKeyspace(),
                UNIQUE_VALUES_TABLE,
                UNIQUE_VALUES_PARTITION_KEYS,
                UNIQUE_VALUES_COLUMN_KEYS,
                UNIQUE_VALUES_COLUMNS,
                TableDefinitionImpl.CacheOption.KEYS,
                UNIQUE_VALUES_CLUSTERING_ORDER);
        }
        return uniqueValues;
    }


    @Override
    protected TableDefinition getEntityUniqueLogTable( CassandraFig cassandraFig ){
        if ( uniqueValuesLog == null ) {
            uniqueValuesLog = new TableDefinitionImpl(  cassandraFig.getApplicationKeyspace(),
                UNIQUE_VALUES_LOG_TABLE,
                UNIQUE_VALUES_LOG_PARTITION_KEYS,
                UNIQUE_VALUES_LOG_COLUMN_KEYS,
                UNIQUE_VALUES_LOG_COLUMNS,
                TableDefinitionImpl.CacheOption.KEYS,
                UNIQUE_VALUES_LOG_CLUSTERING_ORDER);
        }
        return uniqueValuesLog;
    }


    @Override
    protected List<Object> deserializePartitionKey(ByteBuffer bb){


        /**
         *   List<Object> keys = new ArrayList<>(6);
             keys.add(0, appUUID); // UUID
             keys.add(1, applicationType); // String
             keys.add(2, entityType); // String
             keys.add(3, fieldType); // String
             keys.add(4, fieldName); // String
             keys.add(5, fieldValueString); // String

         */

        List<Object> stuff = new ArrayList<>();
        while(bb.hasRemaining()){
            ByteBuffer data = CQLUtils.getWithShortLength(bb);
            if(stuff.size() == 0){
                stuff.add(DataType.uuid().deserialize(data.slice(), ProtocolVersion.NEWEST_SUPPORTED));
            }else{
                stuff.add(DataType.text().deserialize(data.slice(), ProtocolVersion.NEWEST_SUPPORTED));
            }
            byte equality = bb.get(); // we don't use this but take the equality byte off the buffer

        }

        return stuff;

    }

    @Override
    protected ByteBuffer serializeUniqueValueLogColumn(UniqueFieldEntry fieldEntry){

        /**
         *   final UUID version = value.getVersion();
             final Field<?> field = value.getField();

             final FieldTypeName fieldType = field.getTypeName();
             final String fieldValue = field.getValue().toString().toLowerCase();


             DynamicComposite composite = new DynamicComposite(  );

             //we want to sort ascending to descending by version
             composite.addComponent( version,  UUID_SERIALIZER, ColumnTypes.UUID_TYPE_REVERSED);
             composite.addComponent( field.getName(), STRING_SERIALIZER );
             composite.addComponent( fieldValue, STRING_SERIALIZER );
             composite.addComponent( fieldType.name() , STRING_SERIALIZER);
         */

        // values are serialized as strings, not sure why, and always lower cased
        String fieldValueString = fieldEntry.getField().getValue().toString().toLowerCase();


        List<Object> keys = new ArrayList<>(4);
        keys.add(fieldEntry.getVersion());
        keys.add(fieldEntry.getField().getName());
        keys.add(fieldValueString);
        keys.add(fieldEntry.getField().getTypeName().name());

        String comparator = UUID_TYPE_REVERSED;

        int size = 16+fieldEntry.getField().getName().getBytes().length
            +fieldEntry.getField().getValue().toString().getBytes().length+
            fieldEntry.getField().getTypeName().name().getBytes().length;

        // we always need to add length for the 2 byte comparator short,  2 byte length short and 1 byte equality
        size += keys.size()*5;

        // uuid type comparator is longest, ensure we allocate buffer using the max size to avoid overflow
        size += keys.size()*comparator.getBytes().length;

        ByteBuffer stuff = ByteBuffer.allocate(size);


        for (Object key : keys) {

            if(key.equals(fieldEntry.getVersion())) {
                int p = comparator.indexOf("(reversed=true)");
                boolean desc = false;
                if (p >= 0) {
                    comparator = comparator.substring(0, p);
                    desc = true;
                }

                byte a = (byte) 85; // this is the byte value for UUIDType in astyanax used in legacy data
                if (desc) {
                    a = (byte) Character.toUpperCase((char) a);
                }

                stuff.putShort((short) ('耀' | a));
            }else{
                comparator = "UTF8Type"; // only strings are being serialized other than UUIDs here
                stuff.putShort((short)comparator.getBytes().length);
                stuff.put(DataType.serializeValue(comparator, ProtocolVersion.NEWEST_SUPPORTED));
            }

            ByteBuffer kb = DataType.serializeValue(key, ProtocolVersion.NEWEST_SUPPORTED);
            if (kb == null) {
                kb = ByteBuffer.allocate(0);
            }

            // put a short that indicates how big the buffer is for this item
            stuff.putShort((short) kb.remaining());

            // put the actual item
            stuff.put(kb.slice());

            // put an equality byte ( again not used by part of legacy thrift Astyanax schema)
            stuff.put((byte) 0);


        }

        stuff.flip();
        return stuff;

    }

    @Override
    protected ByteBuffer getPartitionKey(Id applicationId, String entityType, String fieldType, String fieldName, Object fieldValue ){

        return serializeKey(applicationId.getUuid(), applicationId.getType(),
            entityType, fieldType, fieldName, fieldValue);

    }

    @Override
    protected ByteBuffer getLogPartitionKey(final Id applicationId, final Id uniqueValueId){

        return serializeLogKey(applicationId.getUuid(), applicationId.getType(),
            uniqueValueId.getUuid(), uniqueValueId.getType());

    }

    @Override
    protected ByteBuffer serializeUniqueValueColumn(EntityVersion entityVersion){

        /**
         *   final Id entityId = ev.getEntityId();
             final UUID entityUuid = entityId.getUuid();
             final String entityType = entityId.getType();

             CompositeBuilder builder = Composites.newDynamicCompositeBuilder();

             builder.addUUID( entityVersion );
             builder.addUUID( entityUuid );
             builder.addString(entityType );
         */

        String comparator = "UTF8Type";

        List<Object> keys = new ArrayList<>(3);
        keys.add(entityVersion.getEntityVersion());
        keys.add(entityVersion.getEntityId().getUuid());
        keys.add(entityVersion.getEntityId().getType());

        // UUIDs are 16 bytes
        int size = 16+16+entityVersion.getEntityId().getType().getBytes().length;

        // we always need to add length for the 2 byte comparator short,  2 byte length short and 1 byte equality
        size += keys.size()*5;

        // we always add comparator to the buffer as well
        size += keys.size()*comparator.getBytes().length;

        ByteBuffer stuff = ByteBuffer.allocate(size);

        for (Object key : keys) {

            // custom comparator alias to comparator mappings in  CQLUtils.COMPOSITE_TYPE ( more leftover from Asytanax )
            // the custom mapping is used for schema creation, but datastax driver does not have the alias concept and
            // we must work with the actual types
            if(key instanceof UUID){
                comparator = "UUIDType";
            }else{
                comparator = "UTF8Type"; // if it's not a UUID, the only other thing we're serializing is text
            }

            stuff.putShort((short)comparator.getBytes().length);
            stuff.put(DataType.serializeValue(comparator, ProtocolVersion.NEWEST_SUPPORTED));

            ByteBuffer kb = DataType.serializeValue(key, ProtocolVersion.NEWEST_SUPPORTED);
            if (kb == null) {
                kb = ByteBuffer.allocate(0);
            }

            // put a short that indicates how big the buffer is for this item
            stuff.putShort((short) kb.remaining());

            // put the actual item
            stuff.put(kb.slice());

            // put an equality byte ( again not used by part of legacy thrift Astyanax schema)
            stuff.put((byte) 0);


        }

        stuff.flip();
        return stuff;

    }

    @Override
    protected List<Object> deserializeUniqueValueColumn(ByteBuffer bb){

        List<Object> stuff = new ArrayList<>();
        int count = 0;
        while(bb.hasRemaining()){

            // pull of custom comparator (per Astyanax deserialize)
            int e = CQLUtils.getShortLength(bb);
            if((e & '耀') == 0) {
                CQLUtils.getBytes(bb, e);
            } else {
                // do nothing
            }


            ByteBuffer data = CQLUtils.getWithShortLength(bb);


            // first two composites are UUIDs, rest are strings
            if(count == 0) {
                stuff.add(new UUID(data.getLong(), data.getLong()));
            }else if(count ==1){
                stuff.add(new UUID(data.getLong(), data.getLong()));
            }else{
                stuff.add(DataType.text().deserialize(data.slice(), ProtocolVersion.NEWEST_SUPPORTED));
            }

            byte equality = bb.get(); // we don't use this but take the equality byte off the buffer

            count++;
        }

        return stuff;

    }

    @Override
    protected List<Object> deserializeUniqueValueLogColumn(ByteBuffer bb){


        /**
         *   List<Object> keys = new ArrayList<>(4);
             keys.add(fieldEntry.getVersion());
             keys.add(fieldEntry.getField().getName());
             keys.add(fieldValueString);
             keys.add(fieldEntry.getField().getTypeName().name());
         */

        List<Object> stuff = new ArrayList<>();
        int count = 0;
        while(bb.hasRemaining()){

            int e = CQLUtils.getShortLength(bb);
            if((e & '耀') == 0) {
                CQLUtils.getBytes(bb, e);
            } else {
                // do nothing
            }

            ByteBuffer data = CQLUtils.getWithShortLength(bb);


            // first composite is a UUID, rest are strings
            if(count == 0) {
                stuff.add(new UUID(data.getLong(), data.getLong()));
            }else{
                stuff.add(DataType.text().deserialize(data.slice(), ProtocolVersion.NEWEST_SUPPORTED));
            }

            byte equality = bb.get(); // we don't use this but take the equality byte off the buffer

            count++;
        }

        return stuff;

    }


    @Override
    public int getImplementationVersion() {
        return CollectionDataVersions.LOG_REMOVAL.getVersion();
    }



    // row key = app UUID + app type + app UUID + app type + field type + field name + field value
    private ByteBuffer serializeKey(UUID appUUID,
                                    String applicationType,
                                    String entityType,
                                    String fieldType,
                                    String fieldName,
                                    Object fieldValue  ){

        // values are serialized as strings, not sure why, and always lower cased
        String fieldValueString = fieldValue.toString().toLowerCase();

        List<Object> keys = new ArrayList<>(6);
        keys.add(0, appUUID);
        keys.add(1, applicationType);
        keys.add(2, entityType);
        keys.add(3, fieldType);
        keys.add(4, fieldName);
        keys.add(5, fieldValueString);


        // UUIDs are 16 bytes, allocate the buffer accordingly
        int size = 16 + applicationType.getBytes().length + entityType.getBytes().length
            + fieldType.getBytes().length + fieldName.getBytes().length+fieldValueString.getBytes().length;


        // we always need to add length for the 2 byte short and 1 byte equality
        size += keys.size()*3;

        ByteBuffer stuff = ByteBuffer.allocate(size);

        for (Object key : keys) {

            ByteBuffer kb = DataType.serializeValue(key, ProtocolVersion.NEWEST_SUPPORTED);
            if (kb == null) {
                kb = ByteBuffer.allocate(0);
            }

            stuff.putShort((short) kb.remaining());
            stuff.put(kb.slice());
            stuff.put((byte) 0);


        }
        stuff.flip();
        return stuff;

    }

    private ByteBuffer serializeLogKey(UUID appUUID, String applicationType, UUID entityId, String entityType){

        List<Object> keys = new ArrayList<>(4);
        keys.add(appUUID);
        keys.add(applicationType);
        keys.add(entityId);
        keys.add(entityType);

        int size = 16+applicationType.getBytes().length+16+entityType.getBytes().length;

        // we always need to add length for the 2 byte short and 1 byte equality
        size += keys.size()*3;

        ByteBuffer stuff = ByteBuffer.allocate(size);

        for (Object key : keys) {

            ByteBuffer kb = DataType.serializeValue(key, ProtocolVersion.NEWEST_SUPPORTED);
            if (kb == null) {
                kb = ByteBuffer.allocate(0);
            }

            stuff.putShort((short) kb.remaining());
            stuff.put(kb.slice());
            stuff.put((byte) 0);


        }
        stuff.flip();
        return stuff;

    }

}
