/*
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you under the Apache License, Version 2.0 (the            *
 * "License"); you may not use this file except in compliance   *
 * with the License.  You may obtain a copy of the License at   *
 *                                                              *
 *   http://www.apache.org/licenses/LICENSE-2.0                 *
 *                                                              *
 * Unless required by applicable law or agreed to in writing,   *
 * software distributed under the License is distributed on an  *
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
 * KIND, either express or implied.  See the License for the    *
 * specific language governing permissions and limitations      *
 * under the License.                                           *
 */
package org.apache.rat.license;

import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Predicate;

import org.apache.rat.analysis.IHeaderMatcher;
import org.apache.rat.analysis.IHeaders;
import org.apache.rat.utils.Log;
import org.apache.rat.utils.ReportingSet;

/**
 * Class to take a set of ILicenses and collection of approved license
 * categories and extract Subsets.
 */
public class LicenseSetFactory {

    /**
     * Search a SortedSet of ILicenseFamily instances looking for a matching instance.
     * @param target The instance to search for.
     * @param licenseFamilies the license families to search.
     * @return the matching instance of the target given.
     */
    public static ILicenseFamily familySearch(final String target, final SortedSet<ILicenseFamily> licenseFamilies) {
        ILicenseFamily family = ILicenseFamily.builder().setLicenseFamilyCategory(target).setLicenseFamilyName("Searching family")
                .build();
        return familySearch(family, licenseFamilies);
    }

    /**
     * Search a SortedSet of ILicenseFamily instances looking for a matching instance.
     * @param target The instance to search for.
     * @param licenseFamilies the license families to search.
     * @return the matching instance of the target given.
     */
    public static ILicenseFamily familySearch(final ILicenseFamily target, final SortedSet<ILicenseFamily> licenseFamilies) {
        SortedSet<ILicenseFamily> part = licenseFamilies.tailSet(target);
        return (!part.isEmpty() && part.first().compareTo(target) == 0) ? part.first() : null;
    }

    /**
     * An enum that defines the types of Licenses to extract.
     */
    public enum LicenseFilter {
        /** All defined licenses are returned. */
        ALL,
        /** Only approved licenses are returned. */
        APPROVED,
        /** No licenses are returned. */
        NONE
    }

    /** The set of defined families. */
    private final ReportingSet<ILicenseFamily> families;
    /** The set of defined licenses */
    private final ReportingSet<ILicense> licenses;

    /** The set of approved license family categories. If the category is not listed, the family is not approved. */
    private final SortedSet<String> approvedLicenseCategories;
    /**
     * The set of license categories that are to be removed from consideration. These are categories that were
     * added but should now be removed.
     */
    private final SortedSet<String> removedLicenseCategories;
    /**
     * The set of approved license ids.  This set contains the set of licenses that are explicitly approved even if
     * the family is not.
     */
    private final SortedSet<String> approvedLicenseIds;
    /**
     * The set of license ids that are to be removed from consideration. This set contains licenses that are to be
     * removed even if the family is approved or if an earlier license approval was granted.
     */
    private final SortedSet<String> removedLicenseIds;

    /**
     * Constructs a factory with the specified set of Licenses and the approved
     * license collection.
     */
    public LicenseSetFactory() {
        families = new ReportingSet<>(new TreeSet<ILicenseFamily>())
                .setMsgFormat(s -> String.format("Duplicate LicenseFamily category: %s", s.getFamilyCategory()));
        licenses = new ReportingSet<>(new TreeSet<ILicense>())
                .setMsgFormat(s -> String.format("Duplicate License %s (%s) of type %s", s.getName(), s.getId(), s.getLicenseFamily().getFamilyCategory()));

        approvedLicenseCategories = new TreeSet<>();
        removedLicenseCategories = new TreeSet<>();
        approvedLicenseIds = new TreeSet<>();
        removedLicenseIds = new TreeSet<>();
    }

    /**
     * Constructs a factory with the specified set of Licenses and the approved
     * license collection.
     * @param licenses the set of defined licenses. Families will be extracted from the licenses.
     */
    public LicenseSetFactory(final SortedSet<ILicense> licenses) {
        this();
        this.licenses.addAll(licenses);
        licenses.forEach(l -> families.addIfNotPresent(l.getLicenseFamily()));
    }

    public void add(final LicenseSetFactory other) {
        this.families.addAll(other.families);
        this.licenses.addAll(other.licenses);
        this.approvedLicenseCategories.addAll(other.approvedLicenseCategories);
        this.removedLicenseCategories.addAll(other.removedLicenseCategories);
        this.approvedLicenseIds.addAll(other.approvedLicenseIds);
        this.removedLicenseIds.addAll(other.removedLicenseIds);
    }

    /**
     * Set the log level for reporting collisions in the set of license families.
     * <p>NOTE: should be set before licenses or license families are added.</p>
     * @param level The log level to use.
     */
    public void logFamilyCollisions(final Log.Level level) {
        families.setLogLevel(level);
    }

    /**
     * Sets the reporting option for duplicate license families.
     * @param state The ReportingSet.Option to use for reporting.
     */
    public void familyDuplicateOption(final ReportingSet.Options state) {
        families.setDuplicateOption(state);
    }

    /**
     * Sets the log level for reporting license collisions.
     * @param level The log level.
     */
    public void logLicenseCollisions(final Log.Level level) {
        licenses.setLogLevel(level);
    }

    /**
     * Sets the reporting option for duplicate licenses.
     * @param state the ReportingSt.Option to use for reporting.
     */
    public void licenseDuplicateOption(final ReportingSet.Options state) {
        licenses.setDuplicateOption(state);
    }

    /**
     * Create a sorted set of licenses families from the collection.
     * @param licenses the collection of all licenses.
     * @return a SortedSet of license families from the collection.
     */
    private static SortedSet<ILicenseFamily> extractFamily(final Collection<ILicense> licenses) {
        SortedSet<ILicenseFamily> result = new TreeSet<>();
        licenses.stream().map(ILicense::getLicenseFamily).forEach(result::add);
        return result;
    }

    /**
     * Adds a license to the list of licenses. Does not add the license to the list
     * of approved licenses.
     * @param license The license to add to the list of licenses.
     */
    public void addLicense(final ILicense license) {
        if (license != null) {
            this.licenses.add(license);
            this.families.addIfNotPresent(license.getLicenseFamily());
        }
    }

    /**
     * Adds a license to the list of licenses. Does not add the license to the list
     * of approved licenses.
     * @param builder The license builder to build and add to the list of licenses.
     * @return The ILicense implementation that was added.
     */
    public ILicense addLicense(final ILicense.Builder builder) {
        if (builder != null) {
            ILicense license = builder.setLicenseFamilies(families).build();
            this.licenses.add(license);
            return license;
        }
        return null;
    }

    /**
     * Adds multiple licenses to the list of licenses. Does not add the licenses to
     * the list of approved licenses.
     * @param licenses The licenses to add.
     */
    public void addLicenses(final Collection<ILicense> licenses) {
        this.licenses.addAll(licenses);
        licenses.stream().map(ILicense::getLicenseFamily).forEach(families::add);
    }

    /**
     * Adds a license family to the list of families. Does not add the family to the
     * list of approved licenses.
     * @param family The license family to add to the list of license families.
     */
    public void addFamily(final ILicenseFamily family) {
        if (family != null) {
            this.families.add(family);
        }
    }

    /**
     * Adds a license family to the list of families. Does not add the family to the
     * list of approved licenses.
     * @param builder The licenseFamily.Builder to build and add to the list of
     * licenses.
     */
    public void addFamily(final ILicenseFamily.Builder builder) {
        if (builder != null) {
            this.families.add(builder.build());
        }
    }

    /**
     * Adds a license family category (id) to the list of approved licenses
     * @param familyCategory the category to add.
     */
    public void approveLicenseCategory(final String familyCategory) {
        approvedLicenseCategories.add(ILicenseFamily.makeCategory(familyCategory));
    }

    /**
     * Adds a license family category (id) to the list of approved licenses
     * @param familyCategory the category to add.
     */
    public void removeLicenseCategory(final String familyCategory) {
        removedLicenseCategories.add(ILicenseFamily.makeCategory(familyCategory));
    }

    /**
     * Adds a license family category (id) to the list of approved licenses
     * @param licenseId the license ID to add.
     */
    public void approveLicenseId(final String licenseId) {
        approvedLicenseIds.add(licenseId);
    }

    /**
     * Removes a license ID from the list of approved licenses.
     * @param licenseId the license ID to remove.
     */
    public void removeLicenseId(final String licenseId) {
        removedLicenseIds.add(licenseId);
    }

    /**
     * Test for approved family category.
     * @param family the license family to test, must be in category format.
     * @return return {@code true} if the category is approved.
     */
    private boolean isApprovedCategory(final ILicenseFamily family) {
        return approvedLicenseCategories.contains(family.getFamilyCategory()) && !removedLicenseCategories.contains(family.getFamilyCategory());
    }

    /**
     * Gets a predicate to filter for approved licenses.
     * @return a predicate that returns {@code true} if the license is approved.
     */
    public Predicate<ILicense> getApprovedLicensePredicate() {
        return lic -> !removedLicenseIds.contains(lic.getId()) && (approvedLicenseIds.contains(lic.getId()) ||
                isApprovedCategory(lic.getLicenseFamily()));
    }

    /**
     * Gets the License objects based on the filter.
     * @param filter the types of LicenseFamily objects to return.
     * @return a SortedSet of ILicense objects.
     */
    public SortedSet<ILicense> getLicenses(final LicenseFilter filter) {
        switch (filter) {
        case ALL:
            return Collections.unmodifiableSortedSet(licenses);
        case APPROVED:
            SortedSet<ILicense> result = new TreeSet<>();
            licenses.stream().filter(getApprovedLicensePredicate()).forEach(result::add);
            return result;
        case NONE:
        default:
            return Collections.emptySortedSet();
        }
    }

    /**
     * Gets the LicenseFamily objects based on the filter.
     * @param filter the types of LicenseFamily objects to return.
     * @return a SortedSet of ILicenseFamily objects.
     */
    public SortedSet<ILicenseFamily> getLicenseFamilies(final LicenseFilter filter) {
        SortedSet<ILicenseFamily> result;
        switch (filter) {
        case ALL:
            result = extractFamily(licenses);
            result.addAll(families);
            return result;
        case APPROVED:
            result = new TreeSet<>();
            licenses.stream().map(ILicense::getLicenseFamily).filter(this::isApprovedCategory).forEach(result::add);
            return result;
        case NONE:
        default:
            return Collections.emptySortedSet();
        }
    }

    /**
     * Gets the License ids based on the filter.
     *
     * @param filter the types of License Ids to return.
     * @return The list of all licenses in the category regardless of whether or not it is used by an ILicense implementation.
     */
    public SortedSet<String> getLicenseCategories(final LicenseFilter filter) {
        SortedSet<String> result = new TreeSet<>();
        switch (filter) {
            case ALL:
                licenses.forEach(l -> result.add(l.getLicenseFamily().getFamilyCategory()));
                families.forEach(f -> result.add(f.getFamilyCategory()));
                result.addAll(approvedLicenseCategories);
                result.addAll(removedLicenseCategories);
                return result;
            case APPROVED:
                approvedLicenseCategories.stream().filter(s -> !removedLicenseCategories.contains(s)).forEach(result::add);
                families.stream().filter(this::isApprovedCategory).forEach(f -> result.add(f.getFamilyCategory()));
                return result;
            case NONE:
            default:
                return Collections.emptySortedSet();
        }
    }

    /**
     * Gets the License ids based on the filter.
     *
     * @param filter the types of License Ids to return.
     * @return The list of all licenses in the category regardless of whether or not it is used by an ILicense implementation.
     */
    public SortedSet<String> getLicenseIds(final LicenseFilter filter) {
        Predicate<ILicense> approved =  l -> (isApprovedCategory(l.getLicenseFamily()) ||
                approvedLicenseIds.contains(l.getId())) && !removedLicenseIds.contains(l.getId());
        SortedSet<String> result = new TreeSet<>();
        switch (filter) {
            case ALL:
                licenses.forEach(l -> result.add(l.getId()));
                result.addAll(approvedLicenseCategories);
                result.addAll(removedLicenseCategories);
                result.addAll(approvedLicenseIds);
                result.addAll(removedLicenseIds);
                return result;
            case APPROVED:
                licenses.stream().filter(approved).forEach(l -> result.add(l.getId()));
                families.stream().filter(this::isApprovedCategory).forEach(f -> result.add(f.getFamilyCategory()));
                approvedLicenseIds.stream().filter(s -> !removedLicenseIds.contains(s)).forEach(result::add);
                return result;
            case NONE:
            default:
                return Collections.emptySortedSet();
        }
    }

    /**
     * Search a SortedSet of licenses for the matching license id.
     *
     * @param licenseId the id to search for.
     * @param licenses the SortedSet of licenses to search.
     * @return the matching license or {@code null} if not found.
     */
    public static Optional<ILicense> search(final String familyId, final String licenseId, final SortedSet<ILicense> licenses) {
        ILicenseFamily searchFamily = ILicenseFamily.builder().setLicenseFamilyCategory(familyId)
                .setLicenseFamilyName("searching proxy").build();
        ILicense target = new ILicense() {

            @Override
            public String getId() {
                return licenseId;
            }

            @Override
            public void reset() {
                // do nothing
            }

            @Override
            public boolean matches(final IHeaders headers) {
                return false;
            }

            @Override
            public boolean equals(final Object o) {
                return ILicense.equals(this, o);
            }

            @Override
            public int hashCode() {
                return ILicense.hash(this);
            }

            @Override
            public ILicenseFamily getLicenseFamily() {
                return searchFamily;
            }

            @Override
            public String getNote() {
                return null;
            }

            @Override
            public String getName() {
                return searchFamily.getFamilyName();
            }

            @Override
            public IHeaderMatcher getMatcher() {
                return null;
            }

        };
        return search(target, licenses);
    }

    /**
     * Search a SortedSet of licenses for the matching license.
     * License must match both family code, and license id.
     *
     * @param target the license to search for. Must not be {@code null}.
     * @param licenses the SortedSet of licenses to search.
     * @return the matching license or {@code null} if not found.
     */
    public static Optional<ILicense> search(final ILicense target, final SortedSet<ILicense> licenses) {
        SortedSet<ILicense> part = licenses.tailSet(target);
        return Optional.ofNullable((!part.isEmpty() && part.first().compareTo(target) == 0) ? part.first() : null);
    }
}
