
package de.ivu.elect.exchange.csv;

import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.inject.Inject;

import org.jopendocument.dom.spreadsheet.Sheet;
import org.jopendocument.dom.spreadsheet.SpreadSheet;
import org.jsefa.csv.annotation.CsvField;

import de.ivu.document.generator.template.ods.builder.OdsDocumentBuilder;
import de.ivu.jsefa.CsvAttachedList;
import de.ivu.jsefa.CsvAttachedListHelper;
import de.ivu.jsefa.CsvFieldComparator;

public class OdsMarshaller<T> {

  @Inject
  private transient ElectCsvHeaderExtractor csvHeaderExtractor;

  public OdsMarshaller() {
    super();
  }

  public List<String> extractHeader(Class< ? > clazz) {
    return csvHeaderExtractor.extractHeader(clazz);
  }

  public void serialize(List<T> objects, OutputStream os) {
    List<String> header = new ArrayList<>();

    if (!objects.isEmpty()) {
      header.addAll(csvHeaderExtractor.extractHeader(objects.get(0).getClass()));
    }

    serialize(objects, os, header);
  }

  public void serialize(List<T> objects, OutputStream os, List<String> header) {
    // Hier jetzt die ODS-Datei erstellen
    OdsDocumentBuilder builder = OdsDocumentBuilder.create(os);

    if (!objects.isEmpty()) {

      SpreadSheet spreadSheet = builder.getSpreadSheet();
      Sheet sheet = spreadSheet.getSheet(0);
      sheet.setName("default-sheet");
      // Header zählt als Zeile, deswegen +1
      sheet.setRowCount(objects.size() + 1);

      writeHeader(sheet, header);

      writeData(objects, sheet);
    }
    builder.save();
  }

  private void writeData(List<T> objects, Sheet sheet) {
    T first = objects.iterator().next();
    if (first != null) {
      boolean hasFieldWithCsvAttachedList = hasFieldWithCsvAttachedList(first);

      List<Field> fields = getCsvFieldsOrderedByPosition(first.getClass());
      setColumnCount(sheet, fields.size());

      for (T dto : objects) {

        // Header zählt als Zeile, deswegen +1
        int rowIndex = objects.indexOf(dto) + 1;

        writeSimpleFields(sheet, fields, dto, rowIndex);
        if (hasFieldWithCsvAttachedList) {
          writeAttachedList(sheet, dto, rowIndex, fields.size());
        }
      }
    }
  }

  private void setColumnCount(Sheet sheet, int size) {
    int currentColumnCount = sheet.getColumnCount();
    if (size > currentColumnCount) {
      sheet.setColumnCount(size);
    }
  }

  private void writeAttachedList(Sheet sheet, T dto, int rowIndex, int colIndexStart) {
    Field field = CsvAttachedListHelper.getFieldWithAttachedListAnnotation(dto);
    field.setAccessible(true);

    List attachedList;

    try {
      attachedList = (List) field.get(dto);
    } catch (Exception e) {
      throw new RuntimeException(e.getMessage(), e);
    }

    int columnIndex = colIndexStart;
    checkColSize(sheet, attachedList, colIndexStart);

    for (Object objectFromList : attachedList) {
      List<Field> csvFields = getCsvFieldsOrderedByPosition(objectFromList.getClass());

      for (Field csvField : csvFields) {
        Object value = null;
        try {
          value = getValue(objectFromList, csvField);
        } catch (Exception e) {
          throw new RuntimeException(e.getMessage(), e);
        }

        if (value != null) {
          sheet.getCellAt(columnIndex, rowIndex).setValue(value);
        } else {
          sheet.getCellAt(columnIndex, rowIndex).setValue(objectFromList);
        }

        columnIndex++;
      }

    }
  }

  // Die Anzahl der Spalten muss dynamisch erhöht werden, da man nicht weiß, wie viele Einträge die Liste hat
  private void checkColSize(Sheet sheet, List columns, int colIndexStart) {
    int sizeOfExtraColumns = columns.size();
    int innerSizeOfExtraColumns = 1;
    if (!columns.isEmpty()) {
      List<Field> csvFields = getCsvFieldsOrderedByPosition(columns.get(0).getClass());
      innerSizeOfExtraColumns = csvFields.size();
    }
    int extraColumnsTotal = innerSizeOfExtraColumns * sizeOfExtraColumns;
    setColumnCount(sheet, colIndexStart + extraColumnsTotal);
  }

  private boolean hasFieldWithCsvAttachedList(Object object) {
    return !CsvAttachedListHelper.getAllFieldsWithAnnotation(object.getClass(), CsvAttachedList.class).isEmpty();
  }

  private void writeSimpleFields(Sheet sheet, List<Field> fields, T dto, int rowIndex) {
    for (int columIndex = 0; columIndex < fields.size(); columIndex++) {
      Field currentField = fields.get(columIndex);
      try {
        Object value = getValue(dto, currentField);
        if (value != null) {
          sheet.getCellAt(columIndex, rowIndex).setValue(value);
        }
      } catch (Exception e) {
        throw new RuntimeException(e.getMessage(), e);
      }
    }
  }

  private void writeHeader(Sheet sheet, List<String> header) {
    sheet.setColumnCount(header.size());
    for (String s : header) {
      if (s != null) {
        sheet.getCellAt(header.indexOf(s), 0).setValue(s);
      }
    }
  }

  private Object getValue(Object dto, Field currentField) throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException {

    Method declaredMethod = dto.getClass().getDeclaredMethod(fieldToGetter(currentField.getName()));

    return declaredMethod.invoke(dto);
  }

  private String fieldToGetter(String name) {
    return "get" + name.substring(0, 1).toUpperCase() + name.substring(1);
  }

  private List<Field> getCsvFieldsOrderedByPosition(Class< ? > objectType) {
    List<Field> declaredFields = CsvAttachedListHelper.getAllFieldsWithAnnotation(objectType, CsvField.class);

    Collections.sort(declaredFields, new CsvFieldComparator());

    return declaredFields;
  }
}
