package org.jabref.logic.layout.format;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;

import org.jabref.logic.layout.AbstractParamLayoutFormatter;
import org.jabref.model.entry.Author;
import org.jabref.model.entry.AuthorList;

import static java.util.function.Predicate.not;

/// Versatile author name formatter that takes arguments to control the formatting style.
public class Authors extends AbstractParamLayoutFormatter {

    /*
    AuthorSort = [FirstFirst | LastFirst | LastFirstFirstFirst]
    AuthorAbbr = [FullName | Initials | FirstInitial | MiddleInitial | InitialsNoSpace | LastName]
    AuthorSep = [Comma | And | Colon | Semicolon | Sep=<string>]
    AuthorLastSep = [And | Comma | Colon | Semicolon | Amp | Oxford | LastSep=<string>]
    AuthorPunc = [FullPunc | NoPunc | NoComma | NoPeriod]
    AuthorNumber = [inf | <number>]
    AuthorNumberEtAl = [ {1} | <number>]
    EtAlString = [ et al. | EtAl=<string>]
     */

    private static final List<String> AUTHOR_ORDER = new ArrayList<>();
    private static final List<String> AUTHOR_ABRV = new ArrayList<>();
    private static final List<String> AUTHOR_PUNC = new ArrayList<>();
    private static final List<String> SEPARATORS = new ArrayList<>();
    private static final List<String> LAST_SEPARATORS = new ArrayList<>();

    private static final Pattern NUMBER_PATTERN = Pattern.compile("[0-9]+");

    static {
        Authors.AUTHOR_ORDER.add("firstfirst");
        Authors.AUTHOR_ORDER.add("lastfirst");
        Authors.AUTHOR_ORDER.add("lastfirstfirstfirst");

        Authors.AUTHOR_ABRV.add("fullname");
        Authors.AUTHOR_ABRV.add("initials");
        Authors.AUTHOR_ABRV.add("firstinitial");
        Authors.AUTHOR_ABRV.add("middleinitial");
        Authors.AUTHOR_ABRV.add("lastname");
        Authors.AUTHOR_ABRV.add("initialsnospace");

        Authors.AUTHOR_PUNC.add("fullpunc");
        Authors.AUTHOR_PUNC.add("nopunc");
        Authors.AUTHOR_PUNC.add("nocomma");
        Authors.AUTHOR_PUNC.add("noperiod");

        Authors.SEPARATORS.add("comma");
        Authors.SEPARATORS.add("and");
        Authors.SEPARATORS.add("colon");
        Authors.SEPARATORS.add("semicolon");
        Authors.SEPARATORS.add("sep");

        Authors.LAST_SEPARATORS.add("and");
        Authors.LAST_SEPARATORS.add("colon");
        Authors.LAST_SEPARATORS.add("semicolon");
        Authors.LAST_SEPARATORS.add("amp");
        Authors.LAST_SEPARATORS.add("oxford");
        Authors.LAST_SEPARATORS.add("lastsep");
    }

    private static final int FIRST_FIRST = 0;
    private static final int LAST_FIRST = 1;
    private static final int LF_FF = 2;

    private static final String COMMA = ", ";
    private static final String AMP = " & ";
    private static final String COLON = ": ";
    private static final String SEMICOLON = "; ";
    private static final String AND = " and ";
    private static final String OXFORD = ", and ";

    private int flMode;

    private boolean abbreviate = true;
    private boolean firstInitialOnly;
    private boolean middleInitial;
    private boolean lastNameOnly;
    private boolean abbrDots = true;
    private boolean abbrSpaces = true;

    private boolean setSep;
    private boolean setMaxAuthors;
    private int maxAuthors = -1;
    private int authorNumberEtAl = 1;

    private String lastFirstSeparator = ", ";
    private String separator = Authors.COMMA;
    private String lastSeparator = Authors.AND;
    private String etAlString = " et al.";

    @Override
    public void setArgument(String arg) {
        List<String> parts = AbstractParamLayoutFormatter.parseArgument(arg);
        for (String part : parts) {
            int index = part.indexOf('=');
            if (index > 0) {
                String key = part.substring(0, index);
                String value = part.substring(index + 1);
                handleArgument(key, value);
            } else {
                handleArgument(part, "");
            }
        }
    }

    private void handleArgument(String key, String value) {
        if (Authors.AUTHOR_ORDER.contains(key.trim().toLowerCase(Locale.ROOT))) {
            if (comp(key, "FirstFirst")) {
                flMode = Authors.FIRST_FIRST;
            } else if (comp(key, "LastFirst")) {
                flMode = Authors.LAST_FIRST;
            } else if (comp(key, "LastFirstFirstFirst")) {
                flMode = Authors.LF_FF;
            }
        } else if (Authors.AUTHOR_ABRV.contains(key.trim().toLowerCase(Locale.ROOT))) {
            if (comp(key, "FullName")) {
                abbreviate = false;
            } else if (comp(key, "Initials")) {
                abbreviate = true;
                firstInitialOnly = false;
            } else if (comp(key, "FirstInitial")) {
                abbreviate = true;
                firstInitialOnly = true;
            } else if (comp(key, "MiddleInitial")) {
                abbreviate = true;
                middleInitial = true;
            } else if (comp(key, "LastName")) {
                lastNameOnly = true;
            } else if (comp(key, "InitialsNoSpace")) {
                abbreviate = true;
                abbrSpaces = false;
            }
        } else if (Authors.AUTHOR_PUNC.contains(key.trim().toLowerCase(Locale.ROOT))) {
            if (comp(key, "FullPunc")) {
                abbrDots = true;
                lastFirstSeparator = ", ";
            } else if (comp(key, "NoPunc")) {
                abbrDots = false;
                lastFirstSeparator = " ";
            } else if (comp(key, "NoComma")) {
                abbrDots = true;
                lastFirstSeparator = " ";
            } else if (comp(key, "NoPeriod")) {
                abbrDots = false;
                lastFirstSeparator = ", ";
            }
        } else if (Authors.SEPARATORS.contains(key.trim().toLowerCase(Locale.ROOT)) || Authors.LAST_SEPARATORS.contains(key.trim().toLowerCase(Locale.ROOT))) {
            // AuthorSep = [Comma | And | Colon | Semicolon | sep=<string>]
            // AuthorLastSep = [And | Comma | Colon | Semicolon | Amp | Oxford | lastsep=<string>]

            if (comp(key, "Comma")) {
                if (setSep) {
                    lastSeparator = Authors.COMMA;
                } else {
                    separator = Authors.COMMA;
                    setSep = true;
                }
            } else if (comp(key, "And")) {
                if (setSep) {
                    lastSeparator = Authors.AND;
                } else {
                    separator = Authors.AND;
                    setSep = true;
                }
            } else if (comp(key, "Colon")) {
                if (setSep) {
                    lastSeparator = Authors.COLON;
                } else {
                    separator = Authors.COLON;
                    setSep = true;
                }
            } else if (comp(key, "Semicolon")) {
                if (setSep) {
                    lastSeparator = Authors.SEMICOLON;
                } else {
                    separator = Authors.SEMICOLON;
                    setSep = true;
                }
            } else if (comp(key, "Oxford")) {
                lastSeparator = Authors.OXFORD;
            } else if (comp(key, "Amp")) {
                lastSeparator = Authors.AMP;
            } else if (comp(key, "Sep") && !value.isEmpty()) {
                separator = value;
                setSep = true;
            } else if (comp(key, "LastSep") && !value.isEmpty()) {
                lastSeparator = value;
            }
        } else if ("etal".equalsIgnoreCase(key.trim())) {
            etAlString = value;
        } else if (Authors.NUMBER_PATTERN.matcher(key.trim()).matches()) {
            // Just a number:
            int num = Integer.parseInt(key.trim());
            if (setMaxAuthors) {
                authorNumberEtAl = num;
            } else {
                maxAuthors = num;
                setMaxAuthors = true;
            }
        }
    }

    /// Check for case-insensitive equality between two strings after removing
    /// white space at the beginning and end of the first string.
    ///
    /// @param one The first string - whitespace is trimmed
    /// @param two The second string
    /// @return true if the strings are deemed equal
    private static boolean comp(String one, String two) {
        return one.trim().equalsIgnoreCase(two);
    }

    @Override
    public String format(String fieldText) {
        if (fieldText == null) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        AuthorList al = AuthorList.parse(fieldText);

        if ((maxAuthors < 0) || (al.getNumberOfAuthors() <= maxAuthors)) {
            for (int i = 0; i < al.getNumberOfAuthors(); i++) {
                Author a = al.getAuthor(i);

                addSingleName(sb, a, (flMode == Authors.FIRST_FIRST) || ((flMode == Authors.LF_FF) && (i > 0)));

                if (i < (al.getNumberOfAuthors() - 2)) {
                    sb.append(separator);
                } else if (i < (al.getNumberOfAuthors() - 1)) {
                    sb.append(lastSeparator);
                }
            }
        } else {
            for (int i = 0; i < Math.min(al.getNumberOfAuthors() - 1, authorNumberEtAl); i++) {
                if (i > 0) {
                    sb.append(separator);
                }
                addSingleName(sb, al.getAuthor(i), flMode == Authors.FIRST_FIRST);
            }
            sb.append(etAlString);
        }

        return sb.toString();
    }

    private void addSingleName(StringBuilder sb, Author a, boolean firstFirst) {
        StringBuilder lastNameSB = new StringBuilder();
        a.getNamePrefix().filter(not(String::isEmpty)).ifPresent(von -> lastNameSB.append(von).append(' '));
        a.getFamilyName().ifPresent(lastNameSB::append);
        String jrSeparator = " ";
        a.getNameSuffix().filter(not(String::isEmpty)).ifPresent(jr -> lastNameSB.append(jrSeparator).append(jr));

        String firstNameResult = "";
        if (a.getGivenName().isPresent()) {
            if (abbreviate) {
                firstNameResult = a.getGivenNameAbbreviated().orElse("");

                if (firstInitialOnly && (firstNameResult.length() > 2)) {
                    firstNameResult = firstNameResult.substring(0, 2);
                } else if (middleInitial) {
                    String abbr = firstNameResult;
                    firstNameResult = a.getGivenName().get();
                    int index = firstNameResult.indexOf(' ');
                    if (index >= 0) {
                        firstNameResult = firstNameResult.substring(0, index + 1);
                        if (abbr.length() > 3) {
                            firstNameResult += abbr.substring(3);
                        }
                    }
                }
                if (!abbrDots) {
                    firstNameResult = firstNameResult.replace(".", "");
                }
                if (!abbrSpaces) {
                    firstNameResult = firstNameResult.replace(" ", "");
                }
            } else {
                firstNameResult = a.getGivenName().get();
            }
        }

        if (lastNameOnly || (firstNameResult.isEmpty())) {
            sb.append(lastNameSB);
        } else if (firstFirst) {
            String firstFirstSeparator = " ";
            sb.append(firstNameResult).append(firstFirstSeparator);
            sb.append(lastNameSB);
        } else {
            sb.append(lastNameSB).append(lastFirstSeparator).append(firstNameResult);
        }
    }
}
