package org.jabref.model.entry;

import java.util.Objects;
import java.util.Optional;

import org.jabref.model.strings.LatexToUnicodeAdapter;
import org.jabref.model.strings.StringUtil;

/**
 * This is an immutable class that keeps information regarding single author. It is just a container for the information, with very simple methods to access it.
 * <p>
 * Current usage: only methods <code>getLastOnly</code>, <code>getFirstLast</code>, and <code>getLastFirst</code> are used; all other methods are provided for completeness.
 */
public class Author {

    /**
     * Object indicating the <code>others</code> author. This is a BibTeX feature mostly rendered in "et al." in LaTeX.
     * Example: <code>authors = {Oliver Kopp and others}</code>. This is then appearing as "Oliver Kopp et al.".
     * In the context of BibTeX key generation, this is "Kopp+" (<code>+</code> for "et al.") and not "KO".
     */
    public static final Author OTHERS = new Author("", "", null, "others", null);

    private final String firstPart;
    private final String firstAbbr;
    private final String vonPart;
    private final String lastPart;
    private final String jrPart;
    private Author latexFreeAuthor;

    /**
     * Creates the Author object. If any part of the name is absent, <CODE>null</CODE> must be passed; otherwise other methods may return erroneous results.
     *
     * @param first     the first name of the author (may consist of several tokens, like "Charles Louis Xavier Joseph" in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin")
     * @param firstabbr the abbreviated first name of the author (may consist of several tokens, like "C. L. X. J." in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin"). It is a responsibility of the caller to create a reasonable abbreviation of the first name.
     * @param von       the von part of the author's name (may consist of several tokens, like "de la" in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin")
     * @param last      the last name of the author (may consist of several tokens, like "Vall{\'e}e Poussin" in "Charles Louis Xavier Joseph de la Vall{\'e}e Poussin")
     * @param jr        the junior part of the author's name (may consist of several tokens, like "Jr. III" in "Smith, Jr. III, John")
     */
    public Author(String first, String firstabbr, String von, String last, String jr) {
        firstPart = addDotIfAbbreviation(removeStartAndEndBraces(first));
        firstAbbr = removeStartAndEndBraces(firstabbr);
        vonPart = removeStartAndEndBraces(von);
        lastPart = removeStartAndEndBraces(last);
        jrPart = removeStartAndEndBraces(jr);
    }

    public static String addDotIfAbbreviation(String name) {
        if ((name == null) || name.isEmpty()) {
            return name;
        }
        // If only one character (uppercase letter), add a dot and return immediately:
        if ((name.length() == 1) && Character.isLetter(name.charAt(0)) &&
                Character.isUpperCase(name.charAt(0))) {
            return name + ".";
        }

        StringBuilder sb = new StringBuilder();
        char lastChar = name.charAt(0);
        for (int i = 0; i < name.length(); i++) {
            if (i > 0) {
                lastChar = name.charAt(i - 1);
            }
            char currentChar = name.charAt(i);
            sb.append(currentChar);

            if (currentChar == '.') {
                // A.A. -> A. A.
                if (((i + 1) < name.length()) && Character.isUpperCase(name.charAt(i + 1))) {
                    sb.append(' ');
                }
            }

            boolean currentIsUppercaseLetter = Character.isLetter(currentChar) && Character.isUpperCase(currentChar);
            if (!currentIsUppercaseLetter) {
                // No uppercase letter, hence nothing to do
                continue;
            }

            boolean lastIsLowercaseLetter = Character.isLetter(lastChar) && Character.isLowerCase(lastChar);
            if (lastIsLowercaseLetter) {
                // previous character was lowercase (probably an acronym like JabRef) -> don't change anything
                continue;
            }

            if ((i + 1) >= name.length()) {
                // Current character is last character in input, so append dot
                sb.append('.');
                continue;
            }

            char nextChar = name.charAt(i + 1);
            if ('-' == nextChar) {
                // A-A -> A.-A.
                sb.append(".");
                continue;
            }
            if ('.' == nextChar) {
                // Dot already there, so nothing to do
                continue;
            }

            // AA -> A. A.
            // Only append ". " if the rest of the 'word' is uppercase
            boolean nextWordIsUppercase = true;
            char furtherChar = Character.MIN_VALUE;
            for (int j = i + 1; j < name.length(); j++) {
                furtherChar = name.charAt(j);
                if (Character.isWhitespace(furtherChar) || (furtherChar == '-') || (furtherChar == '~') || (furtherChar == '.')) {
                    // end of word
                    break;
                }

                boolean furtherIsUppercaseLetter = Character.isLetter(furtherChar) && Character.isUpperCase(furtherChar);
                if (!furtherIsUppercaseLetter) {
                    nextWordIsUppercase = false;
                    break;
                }
            }
            if (nextWordIsUppercase) {
                if (Character.isWhitespace(furtherChar)) {
                    sb.append(".");
                } else {
                    sb.append(". ");
                }
            }
        }

        return sb.toString().trim();
    }

    @Override
    public int hashCode() {
        return Objects.hash(firstAbbr, firstPart, jrPart, lastPart, vonPart);
    }

    /**
     * Compare this object with the given one.
     *
     * @return `true` iff the other object is an Author and all fields are `Objects.equals`.
     */
    @Override
    public boolean equals(Object other) {
        if (this == other) {
            return true;
        }

        if (other instanceof Author that) {
            return Objects.equals(firstPart, that.firstPart)
                    && Objects.equals(firstAbbr, that.firstAbbr)
                    && Objects.equals(vonPart, that.vonPart)
                    && Objects.equals(lastPart, that.lastPart)
                    && Objects.equals(jrPart, that.jrPart);
        }

        return false;
    }

    /**
     * @return true iff the brackets in s are properly paired
     */
    private boolean properBrackets(String s) {
        // nested construct is there, check for "proper" nesting
        int i = 0;
        int level = 0;
        while (i < s.length()) {
            char c = s.charAt(i);
            switch (c) {
                case '{':
                    level++;
                    break;
                case '}':
                    level--;
                    if (level == -1) { // improper nesting
                        return false;
                    }
                    break;
                default:
                    break;
            }
            i++;
        }
        return level == 0;
    }

    /**
     * Removes start and end brace at a string
     * <p>
     * E.g.,
     * <ul>
     *     <li>{Vall{\'e}e Poussin} -> Vall{\'e}e Poussin</li>
     *     <li>{Vall{\'e}e} {Poussin} -> Vall{\'e}e Poussin</li>
     *     <li>Vall{\'e}e Poussin -> Vall{\'e}e Poussin</li>
     * </ul>
     */
    private String removeStartAndEndBraces(String name) {
        if (StringUtil.isBlank(name)) {
            return null;
        }

        if (!name.contains("{")) {
            return name;
        }

        String[] split = name.split(" ");
        StringBuilder b = new StringBuilder();
        for (String s : split) {
            if ((s.length() > 2) && s.startsWith("{") && s.endsWith("}")) {
                // quick solution (which we don't do: just remove first "{" and last "}"
                // however, it might be that s is like {A}bbb{c}, where braces may not be removed

                // inner
                String inner = s.substring(1, s.length() - 1);

                if (inner.contains("}")) {
                    if (properBrackets(inner)) {
                        s = inner;
                    }
                } else {
                    //  no inner curly brackets found, no check needed, inner can just be used as s
                    s = inner;
                }
            }
            b.append(s).append(' ');
        }
        // delete last
        b.deleteCharAt(b.length() - 1);

        // now, all inner words are cleared
        // case {word word word} remains
        // as above, we have to be aware of {w}ord word wor{d} and {{w}ord word word}

        String newName = b.toString();

        if (newName.startsWith("{") && newName.endsWith("}")) {
            String inner = newName.substring(1, newName.length() - 1);
            if (properBrackets(inner)) {
                return inner;
            } else {
                return newName;
            }
        } else {
            return newName;
        }
    }

    /**
     * Returns the first name of the author stored in this object ("First").
     *
     * @return first name of the author (may consist of several tokens)
     */
    public Optional<String> getFirst() {
        return Optional.ofNullable(firstPart);
    }

    /**
     * Returns the abbreviated first name of the author stored in this object ("F.").
     *
     * @return abbreviated first name of the author (may consist of several tokens)
     */
    public Optional<String> getFirstAbbr() {
        return Optional.ofNullable(firstAbbr);
    }

    /**
     * Returns the von part of the author's name stored in this object ("von").
     *
     * @return von part of the author's name (may consist of several tokens)
     */
    public Optional<String> getVon() {
        return Optional.ofNullable(vonPart);
    }

    /**
     * Returns the last name of the author stored in this object ("Last").
     *
     * @return last name of the author (may consist of several tokens)
     */
    public Optional<String> getLast() {
        return Optional.ofNullable(lastPart);
    }

    /**
     * Returns the junior part of the author's name stored in this object ("Jr").
     *
     * @return junior part of the author's name (may consist of several tokens) or null if the author does not have a Jr. Part
     */
    public Optional<String> getJr() {
        return Optional.ofNullable(jrPart);
    }

    /**
     * Returns von-part followed by last name ("von Last"). If both fields were specified as <CODE>null</CODE>, the empty string <CODE>""</CODE> is returned.
     *
     * @return 'von Last'
     */
    public String getLastOnly() {
        if (vonPart == null) {
            return getLast().orElse("");
        } else {
            return lastPart == null ? vonPart : vonPart + ' ' + lastPart;
        }
    }

    /**
     * Returns the author's name in form 'von Last, Jr., First' with the first name full or abbreviated depending on parameter.
     *
     * @param abbr <CODE>true</CODE> - abbreviate first name, <CODE>false</CODE> - do not abbreviate
     * @return 'von Last, Jr., First' (if <CODE>abbr==false</CODE>) or 'von Last, Jr., F.' (if <CODE>abbr==true</CODE>)
     */
    public String getLastFirst(boolean abbr) {
        StringBuilder res = new StringBuilder(getLastOnly());
        getJr().ifPresent(jr -> res.append(", ").append(jr));
        if (abbr) {
            getFirstAbbr().ifPresent(firstA -> res.append(", ").append(firstA));
        } else {
            getFirst().ifPresent(first -> res.append(", ").append(first));
        }
        return res.toString();
    }

    /**
     * Returns the author's name in form 'First von Last, Jr.' with the first name full or abbreviated depending on parameter.
     *
     * @param abbr <CODE>true</CODE> - abbreviate first name, <CODE>false</CODE> - do not abbreviate
     * @return 'First von Last, Jr.' (if <CODE>abbr==false</CODE>) or 'F. von Last, Jr.' (if <CODE>abbr==true</CODE>)
     */
    public String getFirstLast(boolean abbr) {
        StringBuilder res = new StringBuilder();
        if (abbr) {
            getFirstAbbr().map(firstA -> firstA + ' ').ifPresent(res::append);
        } else {
            getFirst().map(first -> first + ' ').ifPresent(res::append);
        }
        res.append(getLastOnly());
        getJr().ifPresent(jr -> res.append(", ").append(jr));
        return res.toString();
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Author{");
        sb.append("firstPart='").append(firstPart).append('\'');
        sb.append(", firstAbbr='").append(firstAbbr).append('\'');
        sb.append(", vonPart='").append(vonPart).append('\'');
        sb.append(", lastPart='").append(lastPart).append('\'');
        sb.append(", jrPart='").append(jrPart).append('\'');
        sb.append('}');
        return sb.toString();
    }

    /**
     * Returns the name as "Last, Jr, F." omitting the von-part and removing starting braces.
     *
     * @return "Last, Jr, F." as described above or "" if all these parts are empty.
     */
    public String getNameForAlphabetization() {
        StringBuilder res = new StringBuilder();
        getLast().ifPresent(res::append);
        getJr().ifPresent(jr -> res.append(", ").append(jr));
        getFirstAbbr().ifPresent(firstA -> res.append(", ").append(firstA));
        while ((res.length() > 0) && (res.charAt(0) == '{')) {
            res.deleteCharAt(0);
        }
        return res.toString();
    }

    /**
     * Returns a LaTeX-free version of this `Author`.
     */
    public Author latexFree() {
        if (latexFreeAuthor == null) {
            String first = getFirst().map(LatexToUnicodeAdapter::format).orElse(null);
            String firstabbr = getFirstAbbr().map(LatexToUnicodeAdapter::format).orElse(null);
            String von = getVon().map(LatexToUnicodeAdapter::format).orElse(null);
            String last = getLast().map(LatexToUnicodeAdapter::format).orElse(null);
            String jr = getJr().map(LatexToUnicodeAdapter::format).orElse(null);
            latexFreeAuthor = new Author(first, firstabbr, von, last, jr);
            latexFreeAuthor.latexFreeAuthor = latexFreeAuthor;
        }
        return latexFreeAuthor;
    }
}
