package net.sf.jabref.logic.layout;

import java.io.File;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import net.sf.jabref.logic.formatter.bibtexfields.HtmlToLatexFormatter;
import net.sf.jabref.logic.formatter.bibtexfields.UnicodeToLatexFormatter;
import net.sf.jabref.logic.layout.format.AuthorAbbreviator;
import net.sf.jabref.logic.layout.format.AuthorAndsCommaReplacer;
import net.sf.jabref.logic.layout.format.AuthorAndsReplacer;
import net.sf.jabref.logic.layout.format.AuthorFirstAbbrLastCommas;
import net.sf.jabref.logic.layout.format.AuthorFirstAbbrLastOxfordCommas;
import net.sf.jabref.logic.layout.format.AuthorFirstFirst;
import net.sf.jabref.logic.layout.format.AuthorFirstFirstCommas;
import net.sf.jabref.logic.layout.format.AuthorFirstLastCommas;
import net.sf.jabref.logic.layout.format.AuthorFirstLastOxfordCommas;
import net.sf.jabref.logic.layout.format.AuthorLF_FF;
import net.sf.jabref.logic.layout.format.AuthorLF_FFAbbr;
import net.sf.jabref.logic.layout.format.AuthorLastFirst;
import net.sf.jabref.logic.layout.format.AuthorLastFirstAbbrCommas;
import net.sf.jabref.logic.layout.format.AuthorLastFirstAbbrOxfordCommas;
import net.sf.jabref.logic.layout.format.AuthorLastFirstAbbreviator;
import net.sf.jabref.logic.layout.format.AuthorLastFirstCommas;
import net.sf.jabref.logic.layout.format.AuthorLastFirstOxfordCommas;
import net.sf.jabref.logic.layout.format.AuthorNatBib;
import net.sf.jabref.logic.layout.format.AuthorOrgSci;
import net.sf.jabref.logic.layout.format.Authors;
import net.sf.jabref.logic.layout.format.CompositeFormat;
import net.sf.jabref.logic.layout.format.CreateBibORDFAuthors;
import net.sf.jabref.logic.layout.format.CreateDocBookAuthors;
import net.sf.jabref.logic.layout.format.CurrentDate;
import net.sf.jabref.logic.layout.format.DOICheck;
import net.sf.jabref.logic.layout.format.DOIStrip;
import net.sf.jabref.logic.layout.format.DateFormatter;
import net.sf.jabref.logic.layout.format.Default;
import net.sf.jabref.logic.layout.format.EntryTypeFormatter;
import net.sf.jabref.logic.layout.format.FileLink;
import net.sf.jabref.logic.layout.format.FirstPage;
import net.sf.jabref.logic.layout.format.FormatPagesForHTML;
import net.sf.jabref.logic.layout.format.FormatPagesForXML;
import net.sf.jabref.logic.layout.format.GetOpenOfficeType;
import net.sf.jabref.logic.layout.format.HTMLChars;
import net.sf.jabref.logic.layout.format.HTMLParagraphs;
import net.sf.jabref.logic.layout.format.IfPlural;
import net.sf.jabref.logic.layout.format.Iso690FormatDate;
import net.sf.jabref.logic.layout.format.Iso690NamesAuthors;
import net.sf.jabref.logic.layout.format.JournalAbbreviator;
import net.sf.jabref.logic.layout.format.LastPage;
import net.sf.jabref.logic.layout.format.LatexToUnicodeFormatter;
import net.sf.jabref.logic.layout.format.NameFormatter;
import net.sf.jabref.logic.layout.format.NoSpaceBetweenAbbreviations;
import net.sf.jabref.logic.layout.format.NotFoundFormatter;
import net.sf.jabref.logic.layout.format.Number;
import net.sf.jabref.logic.layout.format.Ordinal;
import net.sf.jabref.logic.layout.format.RTFChars;
import net.sf.jabref.logic.layout.format.RemoveBrackets;
import net.sf.jabref.logic.layout.format.RemoveBracketsAddComma;
import net.sf.jabref.logic.layout.format.RemoveLatexCommandsFormatter;
import net.sf.jabref.logic.layout.format.RemoveTilde;
import net.sf.jabref.logic.layout.format.RemoveWhitespace;
import net.sf.jabref.logic.layout.format.Replace;
import net.sf.jabref.logic.layout.format.RisAuthors;
import net.sf.jabref.logic.layout.format.RisKeywords;
import net.sf.jabref.logic.layout.format.RisMonth;
import net.sf.jabref.logic.layout.format.ToLowerCase;
import net.sf.jabref.logic.layout.format.ToUpperCase;
import net.sf.jabref.logic.layout.format.WrapContent;
import net.sf.jabref.logic.layout.format.WrapFileLinks;
import net.sf.jabref.logic.layout.format.XMLChars;
import net.sf.jabref.logic.openoffice.OOPreFormatter;
import net.sf.jabref.model.database.BibDatabase;
import net.sf.jabref.model.database.BibDatabaseContext;
import net.sf.jabref.model.entry.BibEntry;
import net.sf.jabref.model.strings.StringUtil;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

class LayoutEntry {
    private static final Log LOGGER = LogFactory.getLog(LayoutEntry.class);
    
    private List<LayoutFormatter> option;

    // Formatter to be run after other formatters:
    private LayoutFormatter postFormatter;

    private String text;

    private List<LayoutEntry> layoutEntries;

    private final int type;

    private final List<String> invalidFormatter = new ArrayList<>();

    private final LayoutFormatterPreferences prefs;

    public LayoutEntry(StringInt si, LayoutFormatterPreferences prefs) {
        this.prefs = prefs;
        type = si.i;
        switch (type) {
        case LayoutHelper.IS_LAYOUT_TEXT:
            text = si.s;
            break;
        case LayoutHelper.IS_SIMPLE_FIELD:
            text = si.s.trim();
            break;
        case LayoutHelper.IS_OPTION_FIELD:
            doOptionField(si.s);
            break;
        case LayoutHelper.IS_FIELD_START:
        case LayoutHelper.IS_FIELD_END:
        default:
            break;
        }
    }

    public LayoutEntry(List<StringInt> parsedEntries, int layoutType, LayoutFormatterPreferences prefs) {
        this.prefs = prefs;
        List<LayoutEntry> tmpEntries = new ArrayList<>();
        String blockStart = parsedEntries.get(0).s;
        String blockEnd = parsedEntries.get(parsedEntries.size() - 1).s;

        if (!blockStart.equals(blockEnd)) {
            LOGGER.warn("Field start and end entry must be equal.");
        }

        type = layoutType;
        text = blockEnd;
        List<StringInt> blockEntries = null;
        for (StringInt parsedEntry : parsedEntries.subList(1, parsedEntries.size() - 1)) {
            switch (parsedEntry.i) {
            case LayoutHelper.IS_FIELD_START:
            case LayoutHelper.IS_GROUP_START:
                blockEntries = new ArrayList<>();
                blockStart = parsedEntry.s;
                break;
            case LayoutHelper.IS_FIELD_END:
            case LayoutHelper.IS_GROUP_END:
                if (blockStart.equals(parsedEntry.s)) {
                    blockEntries.add(parsedEntry);
                    int groupType = parsedEntry.i == LayoutHelper.IS_GROUP_END ? LayoutHelper.IS_GROUP_START :
                            LayoutHelper.IS_FIELD_START;
                    LayoutEntry le = new LayoutEntry(blockEntries, groupType, prefs);
                    tmpEntries.add(le);
                    blockEntries = null;
                } else {
                    LOGGER.warn("Nested field entries are not implemented!");
                }
                break;
            case LayoutHelper.IS_LAYOUT_TEXT:
            case LayoutHelper.IS_SIMPLE_FIELD:
            case LayoutHelper.IS_OPTION_FIELD:
            default:
                // Do nothing
                break;
            }

            if (blockEntries == null) {
                tmpEntries.add(new LayoutEntry(parsedEntry, prefs));
            } else {
                blockEntries.add(parsedEntry);
            }
        }

        layoutEntries = new ArrayList<>(tmpEntries);

        for (LayoutEntry layoutEntry : layoutEntries) {
            invalidFormatter.addAll(layoutEntry.getInvalidFormatters());
        }
    }

    public void setPostFormatter(LayoutFormatter formatter) {
        this.postFormatter = formatter;
    }

    public String doLayout(BibEntry bibtex, BibDatabase database) {
        switch (type) {
        case LayoutHelper.IS_LAYOUT_TEXT:
            return text;
        case LayoutHelper.IS_SIMPLE_FIELD:
            String value = bibtex.getResolvedFieldOrAlias(text, database).orElse("");

            // If a post formatter has been set, call it:
            if (postFormatter != null) {
                value = postFormatter.format(value);
            }
            return value;
        case LayoutHelper.IS_FIELD_START:
        case LayoutHelper.IS_GROUP_START:
            return handleFieldOrGroupStart(bibtex, database);
        case LayoutHelper.IS_FIELD_END:
        case LayoutHelper.IS_GROUP_END:
            return "";
        case LayoutHelper.IS_OPTION_FIELD:
            return handleOptionField(bibtex, database);
        case LayoutHelper.IS_ENCODING_NAME:
            // Printing the encoding name is not supported in entry layouts, only
            // in begin/end layouts. This prevents breakage if some users depend
            // on a field called "encoding". We simply return this field instead:
            return bibtex.getResolvedFieldOrAlias("encoding", database).orElse(null);
        default:
            return "";
        }
    }

    private String handleOptionField(BibEntry bibtex, BibDatabase database) {
        String fieldEntry;

        if (BibEntry.TYPE_HEADER.equals(text)) {
            fieldEntry = bibtex.getType();
        } else if (BibEntry.OBSOLETE_TYPE_HEADER.equals(text)) {
            LOGGER.warn("'" + BibEntry.OBSOLETE_TYPE_HEADER
                    + "' is an obsolete name for the entry type. Please update your layout to use '"
                    + BibEntry.TYPE_HEADER + "' instead.");
            fieldEntry = bibtex.getType();
        } else {
            // changed section begin - arudert
            // resolve field (recognized by leading backslash) or text
            fieldEntry = text.startsWith("\\") ? bibtex
                    .getResolvedFieldOrAlias(text.substring(1), database)
                    .orElse("") : BibDatabase.getText(text, database);
            // changed section end - arudert
        }

        if (option != null) {
            for (LayoutFormatter anOption : option) {
                fieldEntry = anOption.format(fieldEntry);
            }
        }

        // If a post formatter has been set, call it:
        if (postFormatter != null) {
            fieldEntry = postFormatter.format(fieldEntry);
        }

        return fieldEntry;
    }

    private String handleFieldOrGroupStart(BibEntry bibtex, BibDatabase database) {
        Optional<String> field;
        if (type == LayoutHelper.IS_GROUP_START) {
            field = bibtex.getResolvedFieldOrAlias(text, database);
        } else if (text.matches(".*(;|(\\&+)).*")) {
            // split the strings along &, && or ; for AND formatter
            String[] parts = text.split("\\s*(;|(\\&+))\\s*");
            field = Optional.empty();
            for (String part : parts) {
                field = bibtex.getResolvedFieldOrAlias(part, database);
                if (!field.isPresent()) {
                    break;
                }
            }
        } else {
            // split the strings along |, ||  for OR formatter
            String[] parts = text.split("\\s*(\\|+)\\s*");
            field = Optional.empty();
            for (String part : parts) {
                field = bibtex.getResolvedFieldOrAlias(part, database);
                if (field.isPresent()) {
                    break;
                }
            }
        }

        if ((!field.isPresent()) || ((type == LayoutHelper.IS_GROUP_START)
                && field.get().equalsIgnoreCase(LayoutHelper.getCurrentGroup()))) {
            return null;
        } else {
            if (type == LayoutHelper.IS_GROUP_START) {
                LayoutHelper.setCurrentGroup(field.get());
            }
            StringBuilder sb = new StringBuilder(100);
            String fieldText;
            boolean previousSkipped = false;

            for (int i = 0; i < layoutEntries.size(); i++) {
                fieldText = layoutEntries.get(i).doLayout(bibtex, database);

                if (fieldText == null) {
                    if ((i + 1) < layoutEntries.size()) {
                        if (layoutEntries.get(i + 1).doLayout(bibtex, database).trim().isEmpty()) {
                            i++;
                            previousSkipped = true;
                            continue;
                        }
                    }
                } else {

                    // if previous was skipped --> remove leading line
                    // breaks
                    if (previousSkipped) {
                        int eol = 0;

                        while ((eol < fieldText.length())
                                && ((fieldText.charAt(eol) == '\n') || (fieldText.charAt(eol) == '\r'))) {
                            eol++;
                        }

                        if (eol < fieldText.length()) {
                            sb.append(fieldText.substring(eol));
                        }
                    } else {
                        sb.append(fieldText);
                    }
                }

                previousSkipped = false;
            }

            return sb.toString();
        }
    }

    /**
     * Do layout for general formatters (no bibtex-entry fields).
     *
     * @param databaseContext
     *            Bibtex Database
     * @return
     */
    public String doLayout(BibDatabaseContext databaseContext, Charset encoding) {
        switch (type) {
        case LayoutHelper.IS_LAYOUT_TEXT:
            return text;

        case LayoutHelper.IS_SIMPLE_FIELD:
            throw new UnsupportedOperationException("bibtex entry fields not allowed in begin or end layout");

        case LayoutHelper.IS_FIELD_START:
        case LayoutHelper.IS_GROUP_START:
            throw new UnsupportedOperationException("field and group starts not allowed in begin or end layout");

        case LayoutHelper.IS_FIELD_END:
        case LayoutHelper.IS_GROUP_END:
            throw new UnsupportedOperationException("field and group ends not allowed in begin or end layout");

        case LayoutHelper.IS_OPTION_FIELD:
            String field = BibDatabase.getText(text, databaseContext.getDatabase());
            if (option != null) {
                for (LayoutFormatter anOption : option) {
                    field = anOption.format(field);
                }
            }
            // If a post formatter has been set, call it:
            if (postFormatter != null) {
                field = postFormatter.format(field);
            }

            return field;

        case LayoutHelper.IS_ENCODING_NAME:
            return encoding.displayName();

        case LayoutHelper.IS_FILENAME:
            return databaseContext.getDatabaseFile().map(File::getName).orElse("");

        case LayoutHelper.IS_FILEPATH:
            return databaseContext.getDatabaseFile().map(File::getPath).orElse("");

        default:
            break;
        }
        return "";
    }

    private void doOptionField(String s) {
        List<String> v = StringUtil.tokenizeToList(s, "\n");

        if (v.size() == 1) {
            text = v.get(0);
        } else {
            text = v.get(0).trim();

            option = getOptionalLayout(v.get(1));
            // See if there was an undefined formatter:
            for (LayoutFormatter anOption : option) {
                if (anOption instanceof NotFoundFormatter) {
                    String notFound = ((NotFoundFormatter) anOption).getNotFound();

                    invalidFormatter.add(notFound);
                }
            }

        }
    }

    private LayoutFormatter getLayoutFormatterByName(String name) throws Exception {

        switch (name) {
        case "HTMLToLatexFormatter": // For backward compatibility
        case "HtmlToLatex":
            return new HtmlToLatexFormatter();
        case "UnicodeToLatexFormatter": // For backward compatibility
        case "UnicodeToLatex":
            return new UnicodeToLatexFormatter();
        case "OOPreFormatter":
            return new OOPreFormatter();
        case "AuthorAbbreviator":
            return new AuthorAbbreviator();
        case "AuthorAndsCommaReplacer":
            return new AuthorAndsCommaReplacer();
        case "AuthorAndsReplacer":
            return new AuthorAndsReplacer();
        case "AuthorFirstAbbrLastCommas":
            return new AuthorFirstAbbrLastCommas();
        case "AuthorFirstAbbrLastOxfordCommas":
            return new AuthorFirstAbbrLastOxfordCommas();
        case "AuthorFirstFirst":
            return new AuthorFirstFirst();
        case "AuthorFirstFirstCommas":
            return new AuthorFirstFirstCommas();
        case "AuthorFirstLastCommas":
            return new AuthorFirstLastCommas();
        case "AuthorFirstLastOxfordCommas":
            return new AuthorFirstLastOxfordCommas();
        case "AuthorLastFirst":
            return new AuthorLastFirst();
        case "AuthorLastFirstAbbrCommas":
            return new AuthorLastFirstAbbrCommas();
        case "AuthorLastFirstAbbreviator":
            return new AuthorLastFirstAbbreviator();
        case "AuthorLastFirstAbbrOxfordCommas":
            return new AuthorLastFirstAbbrOxfordCommas();
        case "AuthorLastFirstCommas":
            return new AuthorLastFirstCommas();
        case "AuthorLastFirstOxfordCommas":
            return new AuthorLastFirstOxfordCommas();
        case "AuthorLF_FF":
            return new AuthorLF_FF();
        case "AuthorLF_FFAbbr":
            return new AuthorLF_FFAbbr();
        case "AuthorNatBib":
            return new AuthorNatBib();
        case "AuthorOrgSci":
            return new AuthorOrgSci();
        case "CompositeFormat":
            return new CompositeFormat();
        case "CreateBibORDFAuthors":
            return new CreateBibORDFAuthors();
        case "CreateDocBookAuthors":
            return new CreateDocBookAuthors();
        case "CurrentDate":
            return new CurrentDate();
        case "DateFormatter":
            return new DateFormatter();
        case "DOICheck":
            return new DOICheck();
        case "DOIStrip":
            return new DOIStrip();
        case "EntryTypeFormatter":
            return new EntryTypeFormatter();
        case "FirstPage":
            return new FirstPage();
        case "FormatPagesForHTML":
            return new FormatPagesForHTML();
        case "FormatPagesForXML":
            return new FormatPagesForXML();
        case "GetOpenOfficeType":
            return new GetOpenOfficeType();
        case "HTMLChars":
            return new HTMLChars();
        case "HTMLParagraphs":
            return new HTMLParagraphs();
        case "Iso690FormatDate":
            return new Iso690FormatDate();
        case "Iso690NamesAuthors":
            return new Iso690NamesAuthors();
        case "JournalAbbreviator":
            return new JournalAbbreviator(prefs.getJournalAbbreviationLoader(),
                    prefs.getJournalAbbreviationPreferences());
        case "LastPage":
            return new LastPage();
        case "FormatChars": // For backward compatibility
        case "LatexToUnicode":
            return new LatexToUnicodeFormatter();
        case "NameFormatter":
            return new NameFormatter();
        case "NoSpaceBetweenAbbreviations":
            return new NoSpaceBetweenAbbreviations();
        case "Ordinal":
            return new Ordinal();
        case "RemoveBrackets":
            return new RemoveBrackets();
        case "RemoveBracketsAddComma":
            return new RemoveBracketsAddComma();
        case "RemoveLatexCommands":
            return new RemoveLatexCommandsFormatter();
        case "RemoveTilde":
            return new RemoveTilde();
        case "RemoveWhitespace":
            return new RemoveWhitespace();
        case "RisKeywords":
            return new RisKeywords();
        case "RisMonth":
            return new RisMonth();
        case "RTFChars":
            return new RTFChars();
        case "ToLowerCase":
            return new ToLowerCase();
        case "ToUpperCase":
            return new ToUpperCase();
        case "XMLChars":
            return new XMLChars();
        case "Default":
            return new Default();
        case "FileLink":
            return new FileLink(prefs.getFileLinkPreferences());
        case "Number":
            return new Number();
        case "RisAuthors":
            return new RisAuthors();
        case "Authors":
            return new Authors();
        case "IfPlural":
            return new IfPlural();
        case "Replace":
            return new Replace();
        case "WrapContent":
            return new WrapContent();
        case "WrapFileLinks":
            return new WrapFileLinks(prefs.getFileLinkPreferences());
        default:
            return new NotFoundFormatter(name);
        }
    }

    /**
     * Return an array of LayoutFormatters found in the given formatterName
     * string (in order of appearance).
     *
     */
    private List<LayoutFormatter> getOptionalLayout(String formatterName) {

        List<List<String>> formatterStrings = parseMethodsCalls(formatterName);

        List<LayoutFormatter> results = new ArrayList<>(formatterStrings.size());

        Map<String, String> userNameFormatter = NameFormatter.getNameFormatters(prefs.getNameFormatterPreferences());

        for (List<String> strings : formatterStrings) {

            String nameFormatterName = strings.get(0).trim();

            // Check if this is a name formatter defined by this export filter:

            Optional<String> contents = prefs.getCustomExportNameFormatter(nameFormatterName);
            if (contents.isPresent()) {
                NameFormatter nf = new NameFormatter();
                nf.setParameter(contents.get());
                results.add(nf);
                continue;
            }

            // Try to load from formatters in formatter folder
            try {
                LayoutFormatter f = getLayoutFormatterByName(nameFormatterName);
                // If this formatter accepts an argument, check if we have one, and
                // set it if so:
                if ((f instanceof ParamLayoutFormatter) && (strings.size() >= 2)) {
                    ((ParamLayoutFormatter) f).setArgument(strings.get(1));
                }
                results.add(f);
                continue;
            } catch (Exception ex) {
                LOGGER.info("Problem with formatter", ex);
            }

            // Then check whether this is a user defined formatter
            String formatterParameter = userNameFormatter.get(nameFormatterName);

            if (formatterParameter != null) {
                NameFormatter nf = new NameFormatter();
                nf.setParameter(formatterParameter);
                results.add(nf);
                continue;
            }

            results.add(new NotFoundFormatter(nameFormatterName));
        }

        return results;
    }

    public List<String> getInvalidFormatters() {
        return invalidFormatter;
    }

    public static List<List<String>> parseMethodsCalls(String calls) {

        List<List<String>> result = new ArrayList<>();

        char[] c = calls.toCharArray();

        int i = 0;
        while (i < c.length) {

            int start = i;
            if (Character.isJavaIdentifierStart(c[i])) {
                i++;
                while ((i < c.length) && (Character.isJavaIdentifierPart(c[i]) || (c[i] == '.'))) {
                    i++;
                }
                if ((i < c.length) && (c[i] == '(')) {

                    String method = calls.substring(start, i);

                    // Skip the brace
                    i++;
                    int bracelevel = 0;

                    if (i < c.length) {
                        if (c[i] == '"') {
                            // Parameter is in format "xxx"

                            // Skip "
                            i++;

                            int startParam = i;
                            i++;
                            boolean escaped = false;
                            while (((i + 1) < c.length)
                                    && !(!escaped && (c[i] == '"') && (c[i + 1] == ')') && (bracelevel == 0))) {
                                if (c[i] == '\\') {
                                    escaped = !escaped;
                                } else if (c[i] == '(') {
                                    bracelevel++;
                                } else if (c[i] == ')') {
                                    bracelevel--;
                                } else {
                                    escaped = false;
                                }
                                i++;

                            }

                            String param = calls.substring(startParam, i);

                            result.add(Arrays.asList(method, param));
                        } else {
                            // Parameter is in format xxx

                            int startParam = i;

                            while ((i < c.length) && (!((c[i] == ')') && (bracelevel == 0)))) {
                                if (c[i] == '(') {
                                    bracelevel++;
                                } else if (c[i] == ')') {
                                    bracelevel--;
                                }
                                i++;
                            }

                            String param = calls.substring(startParam, i);

                            result.add(Arrays.asList(method, param));
                        }
                    } else {
                        // Incorrectly terminated open brace
                        result.add(Collections.singletonList(method));
                    }
                } else {
                    String method = calls.substring(start, i);
                    result.add(Collections.singletonList(method));
                }
            }
            i++;
        }

        return result;
    }

}
