package com.creditdatamw.zerocell.handler;

import com.creditdatamw.zerocell.ReaderUtil;
import com.creditdatamw.zerocell.ZeroCellException;
import com.creditdatamw.zerocell.ZeroCellReader;
import com.creditdatamw.zerocell.column.ColumnInfo;
import com.creditdatamw.zerocell.converter.Converter;
import com.creditdatamw.zerocell.converter.NoopConverter;
import org.apache.poi.ss.util.CellAddress;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.usermodel.XSSFComment;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.lang.reflect.Field;
import java.util.*;

import static com.creditdatamw.zerocell.converter.ConverterUtils.convertValueToType;

final class EntityExcelSheetHandler<T> implements ZeroCellReader {
    private EntityHandler<T> entityHandler;
    private final Logger LOGGER = LoggerFactory.getLogger(EntityExcelSheetHandler.class);

    private final ColumnInfo rowNumberColumn;
    private final Map<Integer, ColumnInfo> columns;
    private final List<T> entities;
    private final Converter NOOP_CONVERTER = new NoopConverter();
    private final Map<Integer, Converter> converters;
    private boolean isHeaderRow = false;
    private int currentRow = -1;
    private int currentCol = -1;
    private EmptyColumnCounter emptyColumnCounter = new EmptyColumnCounter();
    private T cur;

    EntityExcelSheetHandler(EntityHandler<T> entityHandler, ColumnInfo rowNumberColumn, Map<Integer, ColumnInfo> columns) {
        this.entityHandler = entityHandler;
        this.rowNumberColumn = rowNumberColumn;
        this.columns = columns;
        this.converters = cacheConverters();
        this.entities = new ArrayList<>();
    }

    private Map<Integer, Converter> cacheConverters() {
        Map<Integer, Converter> mappedConverters = new HashMap<>();
        for (ColumnInfo columnInfo : columns.values()) {
            mappedConverters.put(columnInfo.getIndex(), NOOP_CONVERTER);
            try {
                if (!columnInfo.getConverterClass().equals(NoopConverter.class)) {
                    mappedConverters.put(columnInfo.getIndex(), (Converter) columnInfo.getConverterClass().newInstance());
                }
            } catch (InstantiationException | IllegalAccessException e) {
                LOGGER.error("Failed to instantiate Converter class: {}", columnInfo.getConverterClass());
            }
        }
        return mappedConverters;
    }

    /**
     * Returns a list of entities loaded from the provided Excel file
     * @param file the Excel file to load data from
     * @param sheet the sheet to load data from
     * @return list of entities from the sheet
     */
    @Override
    public List<T> read(File file, String sheet) {
        /**
         * We don't need to process the file here since that's
         * handled in {@link ReaderUtil} which MUST be used when using this class
         */
        return Collections.unmodifiableList(this.entities);
    }

    void clear() {
        this.currentRow = -1;
        this.currentCol = -1;
        this.cur = null;
        this.entities.clear();
    }

    @Override
    @SuppressWarnings("unchecked")
    public void startRow(int i) {
        currentRow = i;
        //skip the current row
        if (currentRow - entityHandler.getSkipFirstNRows() < 0) return;
        currentRow = currentRow - entityHandler.getSkipFirstNRows();
        // skip the header row
        if (currentRow == 0) {
            isHeaderRow = true;
            return;
        } else {
            isHeaderRow = false;
        }
        try {
            cur = (T) entityHandler.getEntityClass().newInstance();
            // Write to the field with the @RowNumber annotation here if it exists
            if (!Objects.isNull(rowNumberColumn)) {
                writeColumnField(cur, String.valueOf(currentRow), rowNumberColumn, currentRow);
            }
        } catch (InstantiationException | IllegalAccessException e) {
            throw new ZeroCellException("Failed to create and instance of " + entityHandler.getEntityClass().getName(), e);
        }
    }

    private boolean isRowNumberValueSetted() {
        return entityHandler.getMaxRowNumber() != 0 &&
            entityHandler.getMaxRowNumber() != entityHandler.getSkipFirstNRows();
    }

    @Override
    public void endRow(int i) {
        if (isRowNumberValueSetted() && i > entityHandler.getMaxRowNumber()) {
            return;
        }

        if (!Objects.isNull(cur)) {
            if (entityHandler.isSkipEmptyRows() && emptyColumnCounter.rowIsEmpty()) {
                LOGGER.warn("Row#{} skipped because it is empty", i);
            } else {
                this.entities.add(cur);
            }
        }
        cur = null;
        emptyColumnCounter.reset();
    }

    @Override
    public void cell(String cellReference, String formattedValue, XSSFComment xssfComment) {
        if (cellReference == null) {
            cellReference = new CellAddress(currentRow, currentCol).formatAsString();
        }
        int column = new CellReference(cellReference).getCol();
        currentCol = column;

        ColumnInfo currentColumnInfo = columns.get(column);
        if (Objects.isNull(currentColumnInfo)) {
            return;
        }

        if (isHeaderRow && !entityHandler.isSkipHeaderRow()) {
            if (!currentColumnInfo.getName().equalsIgnoreCase(formattedValue.trim())) {
                throw new ZeroCellException(String.format("Expected Column '%s' but found '%s'", currentColumnInfo.getName(), formattedValue));
            }
        }
        // Prevent from trying to write to a null instance
        if (Objects.isNull(cur)) return;
        if (entityHandler.isSkipEmptyRows()) {
            if (formattedValue == null || formattedValue.isEmpty()) {
                emptyColumnCounter.increment();
            }
        }
        writeColumnField(cur, formattedValue, currentColumnInfo, currentRow);
    }

    /**
     * Write the value read from the excel cell to a field
     *
     * @param object            the object to write to
     * @param formattedValue    the value read from the current excel column/row
     * @param currentColumnInfo Column metadata
     * @param rowNum            the row number
     */
    private void writeColumnField(T object, String formattedValue, ColumnInfo currentColumnInfo, int rowNum) {
        String fieldName = currentColumnInfo.getFieldName();
        try {
            Converter converter = NOOP_CONVERTER;
            if (currentColumnInfo.getIndex() != -1) {
                converter = converters.get(currentColumnInfo.getIndex());
            }
            Object value = null;
            // Don't use a converter if there isn't a custom one
            if (converter instanceof NoopConverter) {
                value = convertValueToType(currentColumnInfo.getType(), formattedValue, currentColumnInfo.getName(), rowNum);
            } else {
                // Handle any exceptions thrown by the converter - this stops execution of the whole process
                try {
                    value = converter.convert(formattedValue, currentColumnInfo.getName(), rowNum);
                } catch (Exception e) {
                    throw new ZeroCellException(String.format("%s threw an exception while trying to convert value %s ", converter.getClass().getName(), formattedValue), e);
                }
            }
            Field field = entityHandler.getEntityClass()
                    .getDeclaredField(currentColumnInfo.getFieldName());
            boolean access = field.isAccessible();
            if (!access) {
                field.setAccessible(true);
            }
            field.set(cur, value);
            field.setAccessible(field.isAccessible() && access);
        } catch (IllegalArgumentException e) {
            throw new ZeroCellException(String.format("Failed to write value %s to field %s at row %s", formattedValue, fieldName, rowNum));
        } catch (NoSuchFieldException | IllegalAccessException e) {
            LOGGER.error("Failed to set field: {}", fieldName, e);
        }
    }

    @Override
    public void headerFooter(String text, boolean b, String tagName) {
        // Skip, no headers or footers in CSV
    }

    private class EmptyColumnCounter {
        private int count = 0;

        void increment() {
            this.count += 1;
        }

        boolean rowIsEmpty() {
            return this.count >= columns.size();
        }
        void reset() {
            count = 0;
        }
    }
}