
package de.ivu.elect.business.person.entity;

import de.ivu.elect.business.anschrift.entity.EntityMitAnschrift;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.mandant.entity.EntityMitMandant;
import de.ivu.elect.business.person.control.AlterBerechnen;
import de.ivu.elect.business.staat.control.StaatByStaatsangehoerigkeitenSortOrderComparator;
import de.ivu.elect.business.staat.entity.AbstractStaat;
import de.ivu.elect.business.staat.entity.Staat;
import de.ivu.elect.business.wahl.entity.Wahl;
import de.ivu.elect.business.wahl.entity.Wahltermin;
import de.ivu.elect.jee.presentation.search.AbstractSearchParameters;
import de.ivu.elect.util.StringListUtil;
import org.apache.commons.lang3.StringUtils;

import javax.persistence.Transient;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import static de.ivu.elect.jee.entity.EntityUtil.normalizeCaseAndDiacritics;
import static java.util.Collections.sort;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

public abstract class AbstractPersonendaten implements PersonendatenBase, EntityMitPersonendaten, EntityMitAnschrift, EntityMitMandant {

    private static final long serialVersionUID = 1L;

    public static final String MESSAGE_KEY_VORNAME_EINZAHL = "EntityMitPersonendatenNamen.MESSAGE_KEY_VORNAME_EINZAHL";

    public static final String MESSAGE_KEY_VORNAME_MEHRZAHL = "EntityMitPersonendatenNamen.MESSAGE_KEY_VORNAME_MEHRZAHL";

    public static final String MESSAGE_KEY_ALTER_EINZAHL = "entity.einzahl.PERSONENDATEN.ALTER";

    public static final String MESSAGE_KEY_ALTER_MEHRZAHL = "entity.mehrzahl.PERSONENDATEN.ALTER";

    public static final String MESSAGE_KEY_PERSONENDATEN_TAG_EINZAHL = "entity.einzahl.PERSONENDATEN.PERSONENDATEN_TAG";

    public static final String MESSAGE_KEY_PERSONENDATEN_TAG_MEHRZAHL = "entity.mehrzahl.PERSONENDATEN.PERSONENDATEN_TAG";

    public static final String ATTRIBUTE_AS_LIST_SEPARATOR = ",";

    public static final String ATTRIBUTE_AS_DISPLAY_STRING_SEPARATOR = ", ";

    public static final int TAG_MAX_LENGTH = 255;

    @Transient
    private transient List<String> berufe;

    @Transient
    private transient List<String> ehrenaemter;

    @Transient
    private transient List<String> akademischeGrade;

    public AbstractPersonendaten() {
        super();
    }

    @Override
    public String getOrdnungsmerkmal() {
        return getExternesOrdnungsmerkmal();
    }

    @Override
    public void setOrdnungsmerkmal(String ordnungsmerkmal) {
        setExternesOrdnungsmerkmal(ordnungsmerkmal);
    }

    public boolean isPersonenregisterAbgeglichen() {
        return PersonenregisterAbgleichStatus.ABGLEICH_DURCHGEFUEHRT.equals(getPersonenregisterAbgleichStatus());
    }

    public boolean isDeutscher() {
        return hasStaatsangehoerigkeit(AbstractStaat.DEUTSCHLAND_LOCALE);
    }

    public boolean isEuBuerger() {
        for (Staat staat : getStaats()) {
            if (Boolean.TRUE.equals(staat.getEuStaat())) {
                return true;
            }
        }

        return false;
    }

    public boolean hasStaatsangehoerigkeit() {
        return !getStaats().isEmpty();
    }

    public boolean hasStaatsangehoerigkeit(Locale locale) {
        String iso3166Alpha2 = locale.getCountry();

        if (isBlank(iso3166Alpha2)) {
            throw new IllegalArgumentException("Locale's ISO-3166-1-ALPHA-2 may NOT be blank");
        }

        for (Staat staatsangehoerigkeit : getStaats()) {
            if (iso3166Alpha2.equals(staatsangehoerigkeit.getIso3166Alpha2())) {
                return true;
            }
        }

        return false;
    }

    public boolean hasStaatsangehoerigkeit(Staat staatsangehoerigkeit) {
        return getStaats().contains(staatsangehoerigkeit);
    }

    public boolean hasChangedStaatsangehoerigkeiten() {
        return hasChangedStaatsangehoerigkeitenSortOrder();
    }

    public abstract boolean hasChangedStaatsangehoerigkeitenSortOrder();

    public Optional<Staat> getFirstStaatsangehoerigkeit() {
        return getStaatsangehoerigkeitenSorted().stream().findFirst();
    }

    public List<Staat> getStaatsangehoerigkeitenSorted() {
        List<Staat> result = new ArrayList<>(getStaats());

        Collections.sort(result, new StaatByStaatsangehoerigkeitenSortOrderComparator(this));

        return result;
    }

    public void addStaatsangehoerigkeit(Staat staatsangehoerigkeit) {
        // Staatsangehörigkeit hinten einsortieren.
        setStaatsangehoerigkeit(getStaatsangehoerigkeitenSorted().size(), staatsangehoerigkeit);
    }

    public void setStaatsangehoerigkeit(int indexZeroBased, Staat staatsangehoerigkeit) {
        if (nonNull(staatsangehoerigkeit) && indexZeroBased > -1) {
            List<Staat> staatsangehoerigkeitenSorted = new ArrayList<>(getStaatsangehoerigkeitenSorted());

            if (indexZeroBased < staatsangehoerigkeitenSorted.size()) {
                staatsangehoerigkeitenSorted.remove(indexZeroBased);
            }

            staatsangehoerigkeitenSorted.add(Math.min(indexZeroBased, staatsangehoerigkeitenSorted.size()), staatsangehoerigkeit);

            setStaatsangehoerigkeitenSorted(staatsangehoerigkeitenSorted);
        }
    }

    public void setStaatsangehoerigkeitenSorted(List<Staat> staatsangehoerigkeitenSorted) {
        removeAllStaats();

        for (Staat staatsangehoerigkeit : staatsangehoerigkeitenSorted) {
            addStaat(staatsangehoerigkeit);
        }

        setStaatsangehoerigkeitenSortOrder(staatsangehoerigkeitenSorted);
    }

    public void removeStaatsangehoerigkeit(Staat staatsangehoerigkeit) {
        List<Staat> staatsangehoerigkeitenSorted = new ArrayList<>(getStaatsangehoerigkeitenSorted());
        staatsangehoerigkeitenSorted.remove(staatsangehoerigkeit);

        removeStaat(staatsangehoerigkeit);

        setStaatsangehoerigkeitenSortOrder(staatsangehoerigkeitenSorted);
    }

    private void setStaatsangehoerigkeitenSortOrder(List<Staat> staatsangehoerigkeitenSorted) {
        setStaatsangehoerigkeitenSortOrder(staatsangehoerigkeitenSorted.stream() //
                .distinct() //
                .map(staatsangehoerigkeit -> staatsangehoerigkeit.getCustomCodeStaatsangehoerigkeit()) //
                .collect(Collectors.joining(ATTRIBUTE_AS_LIST_SEPARATOR)));
    }

    public int getAlter() {
        return getAlter(new Date());
    }

    public int getAlter(Wahltermin wahltermin) {
        return getAlter(wahltermin.getWahldatum());
    }

    public int getAlter(Date someDay) {
        if (hasGeburtsdatum()) {
            return AlterBerechnen.calculateAge(getGeburtsdatum(), someDay);
        }

        return 0;
    }

    public boolean hasGeburtsdatum() {
        return nonNull(getGeburtsdatum());
    }

    public Collection<PersonenVerknuepfung> getPersonenVerknuepfungenByVerknuepfungsArt(VerknuepfungsArt verknuepfungsArt) {
        Collection<PersonenVerknuepfung> result = new ArrayList<>();
        for (PersonenVerknuepfung personenVerknuepfung : getPersonenVerknuepfungen()) {
            if (verknuepfungsArt.equals(personenVerknuepfung.getVerknuepfungsArt())) {
                result.add(personenVerknuepfung);
            }
        }
        return result;
    }

    public Set<PersonendatenPairKey> getPersonendatenPairKeysAusPersonenVerknuepfungen() {
        return PersonenVerknuepfung.mappePersonenVerknuepfungenNachPersonendatenPairKey(getPersonenVerknuepfungen()).keySet();
    }

    public Collection<PersonenVerknuepfung> getPersonenVerknuepfungen() {
        Collection<PersonenVerknuepfung> result = new HashSet<>();

        result.addAll(getPersonenVerknuepfungsByPerson1());
        result.addAll(getPersonenVerknuepfungsByPerson2());

        return result;
    }

    @Transient
    private transient Collection<Personendaten> identischVerknuepftePersonen = null;

    public Collection<Personendaten> getIdentischVerknuepftePersonen() {
        if (identischVerknuepftePersonen == null) {
            identischVerknuepftePersonen = getVerknuepftePersonendatenByVerknuepfungsStatus(VerknuepfungsStatus.IDENTISCH, Personendaten.class);
        }
        return identischVerknuepftePersonen;
    }

    protected <E extends PersonendatenBase> Collection<E> getVerknuepftePersonendatenByVerknuepfungsStatus(VerknuepfungsStatus status, Class<E> personendatenClass) {
        Set<E> result = new HashSet<>();

        addAllVerknuepftePersonendaten(result, status, this, personendatenClass);

        result.remove(this);

        return result;
    }

    private <E extends PersonendatenBase> void addAllVerknuepftePersonendaten(Set<E> result, VerknuepfungsStatus status, PersonendatenBase person, Class<E> personendatenClass) {
        addAllVerknuepftePersonendatenByPerson1(result, status, person, personendatenClass);
        addAllVerknuepftePersonendatenByPerson2(result, status, person, personendatenClass);
    }

    private <E extends PersonendatenBase> void addAllVerknuepftePersonendatenByPerson1(Set<E> result, VerknuepfungsStatus verknuepfungsStatus, PersonendatenBase one, Class<E> personendatenClass) {
        addAllVerknuepftePersonendatenByPerson(result, verknuepfungsStatus, one, personendatenClass, PersonendatenBase::getPersonenVerknuepfungsByPerson1, PersonenVerknuepfung::getPerson2);
    }

    private <E extends PersonendatenBase> void addAllVerknuepftePersonendatenByPerson2(Set<E> result, VerknuepfungsStatus verknuepfungsStatus, PersonendatenBase one, Class<E> personendatenClass) {
        addAllVerknuepftePersonendatenByPerson(result, verknuepfungsStatus, one, personendatenClass, PersonendatenBase::getPersonenVerknuepfungsByPerson2, PersonenVerknuepfung::getPerson1);
    }

    private <E extends PersonendatenBase> void addAllVerknuepftePersonendatenByPerson(Set<E> result, VerknuepfungsStatus verknuepfungsStatus, PersonendatenBase one, Class<E> personendatenClass,
                                                                                      Function<PersonendatenBase, Collection<PersonenVerknuepfung>> personendatenPersonenVerknuepfungenGetter, Function<PersonenVerknuepfung, Personendaten> personenVerknuepfungAnotherGetter) {
        for (PersonenVerknuepfung personenVerknuepfung : personendatenPersonenVerknuepfungenGetter.apply(one)) {
            if (verknuepfungsStatus.equals(personenVerknuepfung.getVerknuepfungsStatus())) {
                Personendaten another = personenVerknuepfungAnotherGetter.apply(personenVerknuepfung);

                if (tryToAddPersonendaten(result, another, personendatenClass) && verknuepfungsStatus.isTansitiv()) {
                    addAllVerknuepftePersonendaten(result, verknuepfungsStatus, another, personendatenClass);
                }
            }
        }
    }

    private <E extends PersonendatenBase> boolean tryToAddPersonendaten(Set<E> result, PersonendatenBase personendaten, Class<E> personendatenClass) {
        if (personendaten.isInstanceOf(personendatenClass)) {
            return result.add(personendaten.castTo(personendatenClass).getEntity());
        }

        return false;
    }

    public Long getGeburtsjahr() {
        if (hasGeburtsdatum()) {
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(getGeburtsdatum());

            return (long) calendar.get(Calendar.YEAR);
        }

        return null;
    }

    protected void preSetWahltermin(Wahltermin alterWert, Wahltermin wahltermin) {
        // nichts zu tun
    }

    protected void postSetWahltermin(Wahltermin alterWert, Wahltermin wahltermin) {

        if (wahltermin == null && hasWahl()) { // zweite Bedingung notwendig, um Endlosschleifen zu vermeiden
            setWahl(null);
        }

        if (wahltermin != null && hasWahl() && !wahltermin.equals(getWahl().getWahltermin())) {
            setWahl(null);
        }
    }

    protected void preSetWahl(Wahl alterWert, Wahl wahl) {
        // nichts zu tun
    }

    protected void postSetWahl(Wahl alterWert, Wahl wahl) {
        if (wahl != null) {
            setWahltermin(wahl.getWahltermin());
        }
    }

    public boolean isPersonStatusUngeprueft() {
        return PersonStatus.UNGEPRUEFT.equals(getPersonStatus());
    }

    public boolean isPersonStatusManuellZuPruefenderKonflikt() {
        return PersonStatus.MANUELL_ZU_PRUEFENDER_KONFLIKT_VORHANDEN.equals(getPersonStatus());
    }

    public boolean isPersonStatusNichtGeeignet() {
        return PersonStatus.NICHT_GEEIGNET.equals(getPersonStatus());
    }

    public boolean isPersonStatusGeeignetOderNichtZuPruefen() {
        return nonNull(getPersonStatus()) && getPersonStatus().isGeeignetOderNichtZuPruefen();
    }

    public boolean isPersonStatusGeeignet() {
        return PersonStatus.GEEIGNET.equals(getPersonStatus());
    }

    public boolean isPersonStatusNichtZuPruefen() {
        return PersonStatus.NICHT_ZU_PRUEFEN.equals(getPersonStatus());
    }

    protected void preSetPersonStatus(PersonStatus alterWert, PersonStatus neuerWert) {
        // nichts zu tun
    }

    protected void postSetPersonStatus(PersonStatus alterWert, PersonStatus neuerWert) {
        // nichts zu tun
    }

    public boolean hasWahl() {
        return getWahl() != null;
    }

    public boolean hasWahltermin() {
        return getWahltermin() != null;
    }

    public boolean hasGebiet() {
        return getGebiet() != null;
    }

    /*
     * NOTE, dass dies die default-Implementierung ist, die ggf. überschrieben wird, bspw. durch das gleichnamige Attribut am Wahlhelfer oder die Komfort-Methode an der Kandidatur. NOTWENDIG ist diese
     * default-Implementierung, um bspw. nach dem Gebiet filtern zu können!
     */
    public Gebiet getGebiet() {
        return null;
    }

    public Date getWahldatum() {
        if (hasWahl()) {
            return getWahl().getWahldatum();
        }

        if (hasWahltermin()) {
            return getWahltermin().getWahldatum();
        }

        return null;
    }

    /**
     * @deprecated PersonendatenFormatter
     */
    @Deprecated
    public String getFamiliennameUndVornameUndTitel() {
        StringBuilder sb = new StringBuilder(getFamilienNameUndTitel());
        if (sb.length() > 0) {
            sb.append(", ");
        }
        if (StringUtils.isNotBlank(getRufname())) {
            sb.append(getRufname());
        }
        return sb.toString();
    }

    /**
     * @deprecated PersonendatenFormatter
     */
    @Deprecated
    private String getFamilienNameUndTitel() {
        StringBuilder sb = new StringBuilder();
        if (StringUtils.isNotBlank(getTitel())) {
            sb.append(getTitel());
            sb.append(" ");
        }
        if (StringUtils.isNotBlank(getFamilienname())) {
            sb.append(getFamilienname());
        }
        return sb.toString();
    }

    /**
     * @deprecated PersonendatenFormatter
     */
    @Deprecated
    public String getTitelVornameNamensbestandteileFamilienname() {
        return append(getTitel(), getRufname(), getFamiliennameBestandteil(), getFamilienname());
    }

    private String append(String... strings) {
        StringBuilder sb = new StringBuilder();
        for (String string : strings) {
            if (isNotBlank(string)) {
                if (sb.length() > 0) {
                    sb.append(" ");
                }
                sb.append(string);
            }
        }
        return sb.toString();
    }

    public static String[] splittMatch(String toMatch) {
        if (StringUtils.isEmpty(toMatch)) {
            return null;
        }
        return toMatch.trim().toLowerCase(Locale.getDefault()).split(" ");
    }

    public boolean isPersonendatenTagVorhanden(String[] splitted) {
        if (splitted == null || splitted.length == 0) {
            return false;
        }
        return matches(splitted, getPersonendatenTagsLower());

    }

    protected static boolean matches(String[] splitted, Set<String> lowerTags) {

        if (splitted[0].contains(AbstractSearchParameters.NEGATION_CHARACTER)) {
            // ELECT-10471
            // Sobald lowerTag.contains(s) zutrifft gibt anyMatch true aus. Der Wert im else Fall kann nicht einfach negiert werden,
            // sonst wird der Fall z.B. splitted = -inaktiv,-gesperrt nicht abgedeckt
            return Arrays.stream(splitted).map(s -> s.replace(AbstractSearchParameters.NEGATION_CHARACTER, "")).
                    anyMatch(s -> !lowerTags.stream().anyMatch(lowerTag -> lowerTag.contains(s)));
        } else {
            return Arrays.stream(splitted).anyMatch(s -> lowerTags.stream().anyMatch(lowerTag -> lowerTag.contains(s)));
        }

    }

    @Transient
    private transient Set<String> personendatenTagsLower = null;

    private Set<String> getPersonendatenTagsLower() {
        if (personendatenTagsLower == null) {
            personendatenTagsLower = getPersonendatenTag().stream().filter(Objects::nonNull).map(s -> s.trim().toLowerCase(Locale.getDefault())).collect(Collectors.toSet());
        }
        return personendatenTagsLower;
    }

    public List<String> getPersonendatenTagsSortiert() {
        List<String> result = new ArrayList<>(getPersonendatenTag());
        sort(result);
        return result;
    }

    protected void preSetBerufUnformatiert(String alterWert, String neuerWert) {
        berufe = null;
    }

    protected void postSetBerufUnformatiert(String alterWert, String neuerWert) {
        berufe = null;
    }

    public List<String> getBerufAsList() {
        if (isNull(berufe)) {
            berufe = splitToList(getBerufUnformatiert());
        }

        return berufe;
    }

    public void setBerufFromList(List<String> berufe) {
        setBerufUnformatiert(joinToAttributeUnformatiert(berufe));
    }

    public String getBerufeCommaSeparated() {
        return joinToDisplayString(getBerufAsList());
    }

    protected void preSetEhrenamtUnformatiert(String alterWert, String neuerWert) {
        ehrenaemter = null;
    }

    protected void postSetEhrenamtUnformatiert(String alterWert, String neuerWert) {
        ehrenaemter = null;
    }

    public List<String> getEhrenamtAsList() {
        if (isNull(ehrenaemter)) {
            ehrenaemter = splitToList(getEhrenamtUnformatiert());
        }

        return ehrenaemter;
    }

    public void setEhrenamtFromList(List<String> ehrenaemter) {
        setEhrenamtUnformatiert(joinToAttributeUnformatiert(ehrenaemter));
    }

    public String getEhrenaemterCommaSeparated() {
        return joinToDisplayString(getEhrenamtAsList());
    }

    protected void preSetAkademischerGradUnformatiert(String alterWert, String neuerWert) {
        akademischeGrade = null;
    }

    protected void postSetAkademischerGradUnformatiert(String alterWert, String neuerWert) {
        akademischeGrade = null;
    }

    public List<String> getAkademischerGradAsList() {
        if (isNull(akademischeGrade)) {
            akademischeGrade = splitToList(getAkademischerGradUnformatiert());
        }

        return akademischeGrade;
    }

    public void setAkademischerGradFromList(List<String> akademischeGrade) {
        setAkademischerGradUnformatiert(joinToAttributeUnformatiert(akademischeGrade));
    }

    public String getAkademischeGradeCommaSeparated() {
        return joinToDisplayString(getAkademischerGradAsList());
    }

    public static List<String> splitToList(String attributeUnformatiert) {
        return StringListUtil.getStringListeByString(attributeUnformatiert, ATTRIBUTE_AS_LIST_SEPARATOR);
    }

    private String joinToAttributeUnformatiert(List<String> attributeAsList) {
        return StringListUtil.getStringByStringListe(attributeAsList, ATTRIBUTE_AS_LIST_SEPARATOR);
    }

    private String joinToDisplayString(List<String> attributeAsList) {
        return StringUtils.join(attributeAsList, ATTRIBUTE_AS_DISPLAY_STRING_SEPARATOR);
    }

    protected String preProcessEhrenamtUnformatiert(String neuerWert) {
        return neuerWert;
    }

    protected String preProcessAkademischerGradUnformatiert(String neuerWert) {
        return neuerWert;
    }

    protected PersonStatus preProcessPersonStatus(PersonStatus neuerWert) {
        return neuerWert;
    }

    protected String preProcessBerufUnformatiert(String neuerWert) {
        return neuerWert;
    }

    protected void preSetFamilienname(String alterWert, String neuerWert) {
        // Do nothing.
    }

    protected String preProcessFamilienname(String neuerWert) {
        // Do nothing.
        return neuerWert;
    }

    protected void postSetFamilienname(String alterWert, String neuerWert) {
        setFamiliennameNormalisiert(normalizeCaseAndDiacritics(neuerWert));
    }

    protected void preSetFamiliennameBestandteil(String alterWert, String neuerWert) {
        // Do nothing.
    }

    protected String preProcessFamiliennameBestandteil(String neuerWert) {
        // Do nothing.
        return neuerWert;
    }

    protected void postSetFamiliennameBestandteil(String alterWert, String neuerWert) {
        setFamiliennameBestandteilNormalisiert(normalizeCaseAndDiacritics(neuerWert));
    }

    protected void preSetRufname(String alterWert, String neuerWert) {
        // Do nothing.
    }

    protected String preProcessRufname(String neuerWert) {
        // Do nothing.
        return neuerWert;
    }

    protected void postSetRufname(String alterWert, String neuerWert) {
        setRufnameNormalisiert(normalizeCaseAndDiacritics(neuerWert));
    }

    protected void preSetVornamen(String alterWert, String neuerWert) {
        // Do nothing.
    }

    protected String preProcessVornamen(String neuerWert) {
        // Do nothing.
        return neuerWert;
    }

    protected void postSetVornamen(String alterWert, String neuerWert) {
        setVornamenNormalisiert(normalizeCaseAndDiacritics(neuerWert));
    }

    public List<String> getEmailAdressenZumVersenden() {
        List<String> emailAdressenZumVersenden = new ArrayList<>();

        if (isNotBlank(getEmailAdresse())) {
            emailAdressenZumVersenden.add(getEmailAdresse());
        }

        if (isNotBlank(getEmailAdresseArbeit())) {
            emailAdressenZumVersenden.add(getEmailAdresseArbeit());
        }

        return emailAdressenZumVersenden;
    }

    @Override
    public EntityMitMandant getParent() {
        Wahl wahl = getWahl();

        if (wahl != null) {
            return wahl;
        }

        Wahltermin wahltermin = getWahltermin();

        if (wahltermin != null) {
            return wahltermin;
        }

        return null;
    }

    public void copyAttributesUsingSetterMitAnschrift(Personendaten ziel) {
        ((Personendaten) this).copyEntityAttributesUsingSetter(ziel);
        getAnschrift().copyEntityAttributesUsingSetter(ziel.getAnschrift());
        getErreichbarkeitsanschrift().copyEntityAttributesUsingSetter(ziel.getErreichbarkeitsanschrift());
    }

}
