package com.tibbo.aggregate.common.datatable.encoding;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;

import com.tibbo.aggregate.common.data.Data;
import com.tibbo.aggregate.common.datatable.DataRecord;
import com.tibbo.aggregate.common.datatable.DataTable;
import com.tibbo.aggregate.common.datatable.FieldFormat;
import com.tibbo.aggregate.common.datatable.SimpleDataTable;
import com.tibbo.aggregate.common.datatable.TableFormat;
import com.tibbo.aggregate.common.datatable.field.DataTableFieldFormat;
import com.tibbo.aggregate.common.datatable.field.DateFieldFormat;
import org.apache.commons.lang3.text.translate.AggregateTranslator;
import org.apache.commons.lang3.text.translate.CharSequenceTranslator;
import org.apache.commons.lang3.text.translate.EntityArrays;
import org.apache.commons.lang3.text.translate.LookupTranslator;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;

public class JsonEncodingHelper
{
  private static final String WRAPPER_FIELD = "wrapper";
  public static final String VALUE_FIELD = "value";
  
  public static String tableToJson(DataTable payload)
  {
    return tableToJson(payload, false);
  }
  
  public static String tableToJson(DataTable payload, boolean convertLongToString)
  {
    return encodeToJson(payload, convertLongToString).toString();
  }

  private static JSONArray encodeToJson(DataTable payload, boolean convertLongToString)
  {
    JSONArray records = new JSONArray();
    
    if (payload == null)
    {
      return records;
    }
    else
    {
      for (DataRecord dr : payload)
      {
        JSONObject record = new JSONObject();
        for (int i = 0; i < dr.getFieldCount(); i++)
        {
          record.put(dr.getFormat(i).getName(), encodeFieldValueToJSON(dr.getFormat(i), dr.getValue(i), convertLongToString));
        }
        records.add(record);
      }
    }
    return records;
  }
  
  private static Object encodeFieldValueToJSON(FieldFormat ff, Object value, boolean convertLongToString)
  {
    switch (ff.getType())
    {
      case FieldFormat.DATATABLE_FIELD:
        return encodeToJson((DataTable) value, convertLongToString);
      
      case FieldFormat.DATE_FIELD:
      case FieldFormat.COLOR_FIELD:
        return ff.valueToString(value);
      case FieldFormat.LONG_FIELD:
        return convertLongToString ? ff.valueToString(value) : value;
      // TODO: Attention! Change this if the encode to json has been broken!
      case FieldFormat.DATA_FIELD:
        Data data = (Data) value;
        return data != null ? data.toJsonString() : "";
      
      case FieldFormat.STRING_FIELD:
        return value;
      
      default:
        return value;
    }
  }

  public static DataTable tableFromJson(String payload, boolean convertUnequalFieldTypesToString) throws Exception
  {
    String jsonText = "{\"" + WRAPPER_FIELD + "\":" + payload + "}"; // wrap initial text to make sure outer element is always JSONObject
    
    JSONParser parser = new JSONParser();
    
    JSONObject json = (JSONObject) parser.parse(jsonText);
    
    return processJSONObject(json, convertUnequalFieldTypesToString);
  }
  
  private static DataTable processJSONObject(JSONObject jsonObject, boolean convertUnequalFieldTypesToString)
  {
    TableFormat tableFormat = calculateTableFormat(jsonObject, convertUnequalFieldTypesToString, true, false);
    DataTable result = new SimpleDataTable(tableFormat);
    
    fillDataTableWithObject(jsonObject, result, true);
    
    return result;
  }
  
  public static TableFormat calculateTableFormat(JSONObject jsonObject, boolean convertUnequalFieldTypesToString, boolean innerDataTable, boolean implicitCasting)
  {
    Collection<FieldFormat> defaultFieldFormats = new HashSet<>();
    return calculateTableFormat(jsonObject, convertUnequalFieldTypesToString, innerDataTable, implicitCasting, defaultFieldFormats);
  }
  
  public static TableFormat calculateTableFormat(JSONObject jsonObject, boolean convertUnequalFieldTypesToString, boolean innerDataTable, boolean implicitCasting,
      Collection<FieldFormat> defaultFieldFormats)
  {
    TableFormat result = new TableFormat(1, 1);
    
    for (Object keyObject : jsonObject.keySet())
    {
      FieldFormat fieldFormat = extractFieldFormatFromNode((String) keyObject, jsonObject.get(keyObject), convertUnequalFieldTypesToString, innerDataTable, implicitCasting, defaultFieldFormats);
      result.addField(fieldFormat);
    }
    
    return result;
  }
  
  public static void fillDataTableWithObject(JSONObject jsonObject, DataTable dataTable, boolean innerDataTable)
  {
    DataRecord dataRecord = dataTable.addRecord();
    
    for (Object keyObject : jsonObject.keySet())
    {
      String key = (String) keyObject;
      Object value = jsonObject.get(key);
      
      if (value instanceof JSONObject && dataRecord.getFormat(key) instanceof DataTableFieldFormat)
      {
        DataTable table = dataRecord.getDataTable(key);
        fillDataTableWithObject((JSONObject) value, table, innerDataTable);
        dataRecord.setValue(key, table);
      }
      else if (value instanceof JSONArray && dataRecord.getFormat(key) instanceof DataTableFieldFormat)
      {
        DataTable table = dataRecord.getDataTable(key);
        fillDataTableWithArray((JSONArray) value, table, innerDataTable);
        dataRecord.setValue(key, table);
      }
      else if (value instanceof String)
      {
        Date date = getDateIfDateFormat(value);
        if (date != null)
        {
          dataRecord.setValue(key, date);
        }
        else
        {
          dataRecord.setValue(key, value);
        }
      }
      else
      {
        dataRecord.setValue(key, value);
      }
    }
  }
  
  public static void fillDataTableWithArray(JSONArray jsonArray, DataTable dataTable, boolean innerDataTable)
  {
    for (Object arrayObject : jsonArray)
    {
      if (!innerDataTable || dataTable.getFormat(VALUE_FIELD) instanceof DataTableFieldFormat)
      {
        DataTable table = dataTable;
        if (innerDataTable)
        {
          DataRecord dataRecord = dataTable.addRecord();
          table = dataRecord.getDataTable(VALUE_FIELD);
          dataRecord.setValue(VALUE_FIELD, table);
        }
        
        if (arrayObject instanceof JSONObject)
        {
          fillDataTableWithObject((JSONObject) arrayObject, table, innerDataTable);
          continue;
        }
        else if (arrayObject instanceof JSONArray)
        {
          fillDataTableWithArray((JSONArray) arrayObject, table, innerDataTable);
          continue;
        }
      }
      
      DataRecord dataRecord = dataTable.addRecord();
      if (dataRecord.hasField(VALUE_FIELD))
      {
        dataRecord.setValue(VALUE_FIELD, arrayObject);
      }
    }
  }
  
  private static FieldFormat extractFieldFormatFromNode(String key, Object node, boolean convertUnequalFieldTypesToString, boolean innerDataTable, boolean implicitCasting,
      Collection<FieldFormat> defaultFieldFormats)
  {
    FieldFormat result = null;
    
    if (node instanceof String)
    {
      Date date = getDateIfDateFormat(node);
      if (date != null)
      {
        result = FieldFormat.create(key, FieldFormat.DATE_FIELD);
      }
      else
      {
        result = FieldFormat.create(key, FieldFormat.STRING_FIELD, true);
      }
    }
    else if (node instanceof Long)
    {
      result = FieldFormat.create(key, FieldFormat.LONG_FIELD);
    }
    else if (node instanceof Boolean)
    {
      result = FieldFormat.create(key, FieldFormat.BOOLEAN_FIELD);
    }
    else if (node instanceof Double)
    {
      result = FieldFormat.create(key, FieldFormat.DOUBLE_FIELD);
    }
    else if (node instanceof JSONArray)
    {
      result = FieldFormat.create(key, FieldFormat.DATATABLE_FIELD);
      
      JSONArray jSONArray = (JSONArray) node;
      LinkedList<FieldFormat> results = new LinkedList<>();
      
      for (int i = 0; i < jSONArray.size(); i++)
      {
        results.add(extractFieldFormatFromNode(VALUE_FIELD, jSONArray.get(i), convertUnequalFieldTypesToString, innerDataTable, implicitCasting, defaultFieldFormats));
      }
      
      FieldFormat innerField = mergeFormats(results, VALUE_FIELD, convertUnequalFieldTypesToString, implicitCasting, defaultFieldFormats);
      TableFormat innerFormat = new TableFormat(innerField);
      DataTable innerTable = new SimpleDataTable(innerFormat);
      if (innerDataTable)
        result.setDefault(innerTable);
      else
        result.setDefault(innerField.getDefaultValue());
    }
    else if (node instanceof JSONObject)
    {
      result = FieldFormat.create(key, FieldFormat.DATATABLE_FIELD);
      
      TableFormat innerFormat = calculateTableFormat((JSONObject) node, convertUnequalFieldTypesToString, innerDataTable, implicitCasting, defaultFieldFormats);
      DataTable innerTable = new SimpleDataTable(innerFormat);
      
      result.setDefault(innerTable);
    }
    else
    {
      result = createDefaultStringFieldFormat(key, defaultFieldFormats);
    }
    
    result.setNullable(true);
    
    return result;
  }
  
  private static Date getDateIfDateFormat(Object node)
  {
    try
    {
      return DateFieldFormat.dateFromString(node.toString());
    }
    catch (Exception e)
    {
      return null;
    }
  }
  
  static FieldFormat createDefaultStringFieldFormat(String fieldName, Collection<FieldFormat> defaultFieldFormats)
  {
    FieldFormat ff = FieldFormat.create(fieldName, FieldFormat.STRING_FIELD).setNullable(true).setDefault(null);
    defaultFieldFormats.add(ff);
    return ff;
  }
  
  static FieldFormat createDefaultDataTableFieldFormat(String fieldName, Collection<FieldFormat> defaultFieldFormats)
  {
    FieldFormat ff = FieldFormat.create(fieldName, FieldFormat.DATATABLE_FIELD);
    defaultFieldFormats.add(ff);
    return ff;
  }
  
  private static FieldFormat mergeFormats(LinkedList<FieldFormat> formats, String fieldName, Boolean convertUnequalFieldTypesToString, boolean implicitCasting,
      Collection<FieldFormat> defaultFieldFormats)
  {
    if (formats.isEmpty())
    {
      return createDefaultDataTableFieldFormat(fieldName, defaultFieldFormats);
    }
    
    if (formats.stream().anyMatch(f -> !(f.getDefaultValue() instanceof DataTable)))
      return chooseFormat(formats, convertUnequalFieldTypesToString, fieldName, defaultFieldFormats);
    
    final FieldFormatDefiner fieldFormatDefiner = new FieldFormatDefiner(convertUnequalFieldTypesToString, implicitCasting, defaultFieldFormats);
    
    HashMap<String, LinkedList<FieldFormat>> fieldsMergeMap = new HashMap<>();
    
    for (FieldFormat format : formats)
    {
      Object def = format.getDefaultValue();
      if (def instanceof DataTable)
      {
        TableFormat tableFormat = ((DataTable) def).getFormat();
        for (FieldFormat ff : tableFormat.getFields())
        {
          final String key = ff.getName();
          if (ff instanceof DataTableFieldFormat)
          {
            if (!fieldsMergeMap.containsKey(key))
              fieldsMergeMap.put(key, new LinkedList<>());
            
            List mergeList = fieldsMergeMap.get(key);
            mergeList.add(ff);
          }
          else
            fieldFormatDefiner.put(key, ff);
        }
      }
    }
    
    for (String key : fieldsMergeMap.keySet())
    {
      fieldFormatDefiner.put(key, mergeFormats(fieldsMergeMap.get(key), key, convertUnequalFieldTypesToString, implicitCasting, defaultFieldFormats));
    }
    
    final TableFormat mergedFormat = new TableFormat();
    for (String key : fieldFormatDefiner.getFieldNames())
    {
      mergedFormat.addField(fieldFormatDefiner.get(key));
    }
    
    FieldFormat format = FieldFormat.create(fieldName, FieldFormat.DATATABLE_FIELD);
    format.setDefault(new SimpleDataTable(mergedFormat));
    format.setNullable(true);
    return format;
  }
  
  private static FieldFormat chooseFormat(List<FieldFormat> fieldFormats, Boolean convertUnequalFieldTypesToString, String fieldName, Collection<FieldFormat> defaultFieldFormat)
  {
    if (convertUnequalFieldTypesToString && !isEqualFieldTypes(fieldFormats))
    {
      FieldFormat ff = FieldFormat.create(fieldName, FieldFormat.STRING_FIELD);
      ff.setNullable(true);
      return ff;
    }
    
    for (FieldFormat ff : fieldFormats)
    {
      if (!isDefaultFieldFormat(ff, defaultFieldFormat))
        return ff;
    }
    
    return fieldFormats.get(0);
  }
  
  private static Boolean isEqualFieldTypes(List<FieldFormat> fieldFormats)
  {
    char type = fieldFormats.get(0).getType();
    for (FieldFormat ff : fieldFormats)
    {
      if (ff.getType() != type)
        return false;
    }
    return true;
  }
  
  static boolean isDefaultFieldFormat(FieldFormat ff, Collection<FieldFormat> defaultFieldFormats)
  {
    return defaultFieldFormats.contains(ff);
  }
  
  public static class EscapeJsonWithoutUnicode
  {
    private static final CharSequenceTranslator ESCAPE_JSON_WITHOUT_UNICODE = new AggregateTranslator(new LookupTranslator(new String[][] { { "\"", "\\\"" }, { "\\", "\\\\" } }),
        new LookupTranslator(EntityArrays.JAVA_CTRL_CHARS_ESCAPE()));
    
    public static String translate(String input)
    {
      return ESCAPE_JSON_WITHOUT_UNICODE.translate(input);
    }
  }
}
