package com.capgemini.reports.definition.r20report;

import com.capgemini.framework.common.HashMapComparingStringIDs;
import com.capgemini.framework.common.HashMapComparingStringIDs.IdMaker;
import com.capgemini.framework.teamcenter.authent.TcSession;
import com.capgemini.framework.teamcenter.exceptions.EmptyNomenclatureException;
import com.capgemini.framework.teamcenter.exceptions.KurtException;
import com.capgemini.framework.teamcenter.exceptions.NoSuchBomViewException;
import com.capgemini.framework.teamcenter.object.pom.workspace.bom.TcBomObject;
import com.capgemini.framework.teamcenter.object.pom.workspace.bom.TcBomObjectFactory;
import com.capgemini.framework.teamcenter.object.pom.workspace.bom.TcRevisionRule;
import com.capgemini.framework.teamcenter.object.pom.workspace.bom.TcRevisionRuleProperties;
import com.capgemini.framework.teamcenter.object.pom.workspace.bom.product.TcProduct;
import com.capgemini.framework.teamcenter.object.runtime.TcBomWindow;
import com.capgemini.framework.teamcenter.object.runtime.TcBomWindowConfiguration;
import com.capgemini.framework.teamcenter.object.runtime.TcRevisionRulesConfiguration;
import com.capgemini.framework.teamcenter.queries.saved.modelobject.ItemRevisionQuery;
import com.capgemini.reports.bomlineholder.BOMLineHolder;
import com.capgemini.reports.bomlineholder.BOMLineHolderFactory;
import com.capgemini.reports.changeobjects.UnitRevision;
import com.capgemini.reports.changeobjects.change.TcChangeNoticeRevision;
import com.capgemini.reports.changeobjects.change.TcChangeRequestRevision;
import com.capgemini.reports.definition.r20report.ExecutionR20.ReportDataR20;
import com.capgemini.reports.definition.r20report.comparator.ChangeComparator;
import com.capgemini.reports.definition.r20report.comparator.GenericComparator;
import com.capgemini.reports.definition.r20report.printing.R20Printer;
import com.capgemini.reports.exceptions.MalformedNomenclatureException;
import com.capgemini.reports.exceptions.NotFoundException;
import com.capgemini.reports.execution.IDataReportPreparator;
import com.capgemini.reports.execution.strategies.IExecutionStrategy;
import com.capgemini.reports.filter.AllIsGoodFilter;
import com.capgemini.reports.printing.PrintParameters;
import com.capgemini.reports.reports.parameters.Generics;
import com.capgemini.reports.reports.parameters.ProductUnit;
import com.capgemini.reports.snecmaobjects.generic.AbstractGeneric;
import com.capgemini.reports.snecmaobjects.generic.Generic;
import com.capgemini.reports.snecmaobjects.generic.GenericAssemb;
import com.capgemini.reports.snecmaobjects.part.PartAeroRevision;
import com.capgemini.reports.snecmaobjects.product.ProductRevision;
import com.capgemini.reports.teamcenter.queries.saved.ChangeRequestQuery;
import com.capgemini.reports.teamcenter.resolution.structureservice.SingleStructureServiceCaller;
import com.capgemini.reports.teamcenter.resolution.structureservice.explorer.PartNumberFinder;
import com.teamcenter.soa.client.model.strong.ItemRevision;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

import static com.capgemini.framework.teamcenter.object.types.SnecmaObjectTypes.GENERIC;


/**
 * This class will allow to prepare data and findAll back data for making report R20.
 */
public class DataReportPreparatorR20 implements IDataReportPreparator<ExecutionR20> {
    private static final long serialVersionUID = -2545511620931881872L;
    private static final Logger LOGGER = LoggerFactory.getLogger(DataReportPreparatorR20.class);


    /**
     * Comparator used by all the {@link Set}s of {@link TcChangeRequestRevision}, which only uses Item IDs to compare and no revision, so that two revisions of a same Item are recognised as equal.
     */
    private static final Comparator<TcChangeRequestRevision> CHANGE_COMPARATOR = new ChangeComparator();

    /**
     * Comparator used by all the Sets, which only uses Item IDs to compare, so that two revisions of a same Item are recognized as equal.
     */
    private static final Comparator<AbstractGeneric> GENERIC_COMPARATOR = new GenericComparator();

    /**
     * Class of a {@link Set} of {@link TcChangeRequestRevision}.
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private static final Class<Set<TcChangeRequestRevision>> CLASS_LIST_CHANGE = (Class) Set.class;

    // Sets of generics
    /**
     * This IdMaker uses only the Generic's ID to make an identifier, not his Revision.
     */
    private static final IdMaker<AbstractGeneric> GENERIC_ID_MAKER = new IdMaker<AbstractGeneric>() {
        private static final long serialVersionUID = 5794106087600351583L;

        @Override
        public String makeId(final AbstractGeneric gen) {
            return gen.getItemId();
        }
    };
    // Unit After
    /**
     * {@link SortedSet} of all the Generics.
     */
    private final SortedSet<AbstractGeneric> totalGenerics = new TreeSet<>(GENERIC_COMPARATOR);
    /**
     * {@link Map} of all the Generics under the Unit After. Points to the (maybe empty) list of {@link TcChangeRequestRevision} that made a change to key generic.
     */
    private final Map<AbstractGeneric, Set<TcChangeRequestRevision>> totalGenericsAfter =
            new HashMapComparingStringIDs<>(GENERIC_ID_MAKER, AbstractGeneric.class, CLASS_LIST_CHANGE);
    /**
     * {@link Set} of all the Generics under the Unit After, which do not have any changes from previous unit.
     */
    private final Set<AbstractGeneric> totalGenericsAfterNoMod = new TreeSet<>(GENERIC_COMPARATOR);
    /**
     * {@link Map} of all the Generics under the Unit After, which undergo a rupture of interchangeability. Points to the non-empty list of {@link TcChangeRequestRevision} that made a change to key generic.
     */
    private final Map<AbstractGeneric, Set<TcChangeRequestRevision>> totalGenericsAfterWithRupture =
            new HashMapComparingStringIDs<>(GENERIC_ID_MAKER, AbstractGeneric.class, CLASS_LIST_CHANGE);
    // Unit Before
    /**
     * {@link Map} of all the Generics under the Unit After, which do not have a rupture of interchangeability.Points to the non-empty list of {@link TcChangeRequestRevision} that made a change to key generic.
     */
    private final Map<AbstractGeneric, Set<TcChangeRequestRevision>> totalGenericsAfterWithoutRupture =
            new HashMapComparingStringIDs<>(GENERIC_ID_MAKER, AbstractGeneric.class, CLASS_LIST_CHANGE);
    /**
     * {@link Map} of all the Generics under the Unit Before. Points to the (maybe empty) list of {@link TcChangeRequestRevision} that made a change to key generic.
     */
    private final Map<AbstractGeneric, Set<TcChangeRequestRevision>> totalGenericsBefore =
            new HashMapComparingStringIDs<>(GENERIC_ID_MAKER, AbstractGeneric.class, CLASS_LIST_CHANGE);
    /**
     * {@link Set} of all the Generics under the Unit Before, which do not have any changes made to them in the unit after.
     */
    private final Set<AbstractGeneric> totalGenericsBeforeNoMod = new TreeSet<>(GENERIC_COMPARATOR);
    /**
     * {@link Map} of all the Generics mentioned by a Removed Modification (LMR). Points to the non-empty list of {@link TcChangeRequestRevision} Points to the non-empty list of {@link TcChangeRequestRevision} from the LMR that mentionned this key generic.
     */
    private final Map<AbstractGeneric, Set<TcChangeRequestRevision>> totalGenericsBeforeLMR =
            new HashMapComparingStringIDs<>(GENERIC_ID_MAKER, AbstractGeneric.class, CLASS_LIST_CHANGE);
    /**
     * {@link Map} of all the Generics under the Unit Before, which undergo a removed modification with rupture. Points to the non-empty {@link Set} of {@link TcChangeRequestRevision} from the LMR that mentionned this key generic.
     */
    private final Map<AbstractGeneric, Set<TcChangeRequestRevision>> totalGenericsBeforeLMRWithRupture =
            new HashMapComparingStringIDs<>(GENERIC_ID_MAKER, AbstractGeneric.class, CLASS_LIST_CHANGE);

    // Sets of Change Requests
    /**
     * {@link Map} of all the Generics under the Unit Before, which undergo a removed modification without rupture (from the LMR that mentionned this key generic).
     */
    private final Map<AbstractGeneric, Set<TcChangeRequestRevision>> totalGenericsBeforeLMRWithoutRupture =
            new HashMapComparingStringIDs<>(GENERIC_ID_MAKER, AbstractGeneric.class, CLASS_LIST_CHANGE);
    /**
     * The List of Removed Modifications objects, LMR or not.
     */
    private final Set<TcChangeRequestRevision> totalChangeRequest = new TreeSet<>(CHANGE_COMPARATOR);
    /**
     * The List of Removed Modifications objects found in the unit After.
     */
    private final Set<TcChangeRequestRevision> totalChangeRequestNotLMR = new TreeSet<>(CHANGE_COMPARATOR);
    /**
     * The List of Removed Modifications objects that the user requested as LMR.
     */
    private final Set<TcChangeRequestRevision> totalChangeRequestLMR = new TreeSet<>(CHANGE_COMPARATOR);

    /**
     * Retrieve the PartNumbers of the generics using a Nomenclature Explosion, and associate them in a map.
     */
    private static Map<AbstractGeneric, PartAeroRevision> retrieveGenericsAndPartNumbers(final TcSession session,
                                                                                         ExecutionR20 report, final Set<AbstractGeneric> setOfGenerics, final ProductUnit productUnit) throws KurtException {

        Map<AbstractGeneric, PartAeroRevision> genericsAndPartNumber =
                new HashMapComparingStringIDs<>(GENERIC_ID_MAKER, AbstractGeneric.class, PartAeroRevision.class);

        for (AbstractGeneric generic : setOfGenerics) {
            // Setup parameters
            Generics generics;
            if (generic instanceof Generic) {
                generics = new Generics(productUnit, generic.getItemId(), null);
            } else if (generic instanceof GenericAssemb) {
                generics = new Generics(productUnit, null, generic.getItemId());
            } else {
                LOGGER.warn("Object should be a Generic but is a: {}", generic.getType());
                continue;
            }

            TcBomObject topLineObject = new TcBomObjectFactory(session, generics.getItem(session)).create();
            TcProduct productObject = new TcProduct(session, productUnit.getProduct(session));
            int unit = productUnit.getUnit();

            TcRevisionRuleProperties revisionRuleProperties = new TcRevisionRuleProperties(productObject, unit);

            TcRevisionRule revisionRule = session.findRevRule(report.getRevisionRule());
            TcRevisionRulesConfiguration revRulesConfiguration = new TcRevisionRulesConfiguration(revisionRule, revisionRuleProperties);

            // Perform the job
            SingleStructureServiceCaller theStructCaller = new SingleStructureServiceCaller(session);
            try (TcBomWindow bomWindow = session.createBomWindow(new TcBomWindowConfiguration(topLineObject, revRulesConfiguration))) {
                BOMLineHolder unbuiltPartAeroHolder = theStructCaller.resolve(bomWindow.topLine(), new PartNumberFinder());
                if (unbuiltPartAeroHolder == null) {
                    LOGGER.warn("Nomenclature explosion failed to obtain a Part Number for Generic={}, product={} and Unit={}", generic.getItemId(),
                            productUnit.getProductID(), productUnit.getUnit());
                    continue;
                }

                // Build Part number
                BOMLineHolder partAeroHolder = BOMLineHolderFactory.buildTopBom(session, unbuiltPartAeroHolder, new AllIsGoodFilter());
                unbuiltPartAeroHolder.freeze();

                // Put the PartNumber in the Map
                genericsAndPartNumber.put(generic, (PartAeroRevision) partAeroHolder.getSnecmaObject());
            } catch (EmptyNomenclatureException e) {
                String msg = String.format("Nomenclature explosion failed to obtain the GenericBom for Generic=%s, product=%s and Unit=%d",
                        generic.getItemId(), productUnit.getProductID(), productUnit.getUnit());
                LOGGER.error(msg, e);
            } catch (NoSuchBomViewException e) {
                LOGGER.error("Unable to find the GenericBom of the Generic: " + generic.getItemId(), e);
            } catch (MalformedNomenclatureException e) {
                LOGGER.error("Unable to find the Part Number of the Generic: " + generic.getItemId(), e);
            }
        }
        return genericsAndPartNumber;
    }

    @Override
    public String print(TcSession session, ExecutionR20 execution, IExecutionStrategy executionStrategy) throws KurtException {
        PrintParameters printParameters = prepareData(session, execution);
        executionStrategy.print(printParameters, execution);
        return executionStrategy.getReportFileName(execution) + printParameters.getOuputFileExtension();
    }

    private PrintParameters prepareData(final TcSession session, final ExecutionR20 report) throws KurtException {
        LOGGER.debug("PrepareData started on: {}", report.getReportIdentifier().getReportTitle());

        session.setObjectPropPolicy(report.getFullPolicy());

        // Config ProduitUnits
        report.getProductUnitBefore().parseUserUnit();
        report.getProductUnitAfter().parseUserUnit();

        // Retrieve the Items of the provided LMR
        retrieveLMR(session, report);

        // Build ProductRevision and UnitRevision before and after
        ProductRevision product = retrieveProductRevisionBefore(session, report);
        UnitRevision unitBefore = retrieveUnitBefore(report, product);
        UnitRevision unitAfter = retrieveUnitAfter(report, product);
        ReportDataR20 data = new ReportDataR20(product, unitBefore, unitAfter, report.getMatriculeMoteur(), report.getClasse(),
                report.getMajorMinor(), report.getConjointUnique());

        // Build ChangeRequests and generics
        retrieveChangeRequests(unitAfter);
        retrieveAllGenerics(unitBefore, unitAfter);

        // Put all retrieved data in the ReportData object
        data.setGenericsAndPartNumbersAfter(retrieveGenericsAndPartNumbers(session, report, totalGenericsAfter.keySet(), report.getProductUnitAfter()));
        data.setGenericsAndPartNumbersBefore(retrieveGenericsAndPartNumbers(session, report, totalGenericsBefore.keySet(), report.getProductUnitBefore()));
        data.setTotalChangeRequest(totalChangeRequest);
        data.setTotalChangeRequestLMR(totalChangeRequestLMR);
        data.setTotalChangeRequestNotLMR(totalChangeRequestNotLMR);
        data.setTotalGenerics(totalGenerics);
        data.setTotalGenericsAfterWithoutRupture(totalGenericsAfterWithoutRupture);
        data.setTotalGenericsAfterWithRupture(totalGenericsAfterWithRupture);
        data.setTotalGenericsBeforeLMRWithoutRupture(totalGenericsBeforeLMRWithoutRupture);
        data.setTotalGenericsBeforeLMRWithRupture(totalGenericsBeforeLMRWithRupture);

        R20Printer printer = new R20Printer();
        PrintParameters printParameters = printer.makeMasterTemplate(report, data);
        printer.constructDataMaps(session, printParameters, data);

        LOGGER.debug("PrepareData ended on: {}", report.getReportIdentifier().getReportTitle());

        return printParameters;
    }

    UnitRevision retrieveUnitAfter(final ExecutionR20 report, final ProductRevision product) {
        return UnitRevision.chooseRevisionOfUnitFromRange(report.getProductUnitAfter().getUnitStr(), product.getUnits(),
                ExecutionR20.R20_STATUS_AFTER);
    }

    private UnitRevision retrieveUnitBefore(final ExecutionR20 report, final ProductRevision product) {
        return UnitRevision.chooseRevisionOfUnitFromRange(report.getProductUnitBefore().getUnitStr(), product.getUnits(),
                ExecutionR20.R20_STATUS_BEFORE);
    }

    /**
     * This method uses the lmrString list of Modification IDs to produce a {@link List} of {@link TcChangeRequestRevision}. <br>
     * these {@link TcChangeRequestRevision} are built using TC objects queried from the input IDs.
     *
     * @param session the session to connect to TC
     * @param report  will provide the informations of the report which is executing
     */
    private void retrieveLMR(final TcSession session, final ExecutionR20 report) {
        for (String changeID : report.getLmrList()) {
            List<ItemRevision> list = new ChangeRequestQuery(session).searchID(changeID).findAll();
            for (ItemRevision itemRev : list) {
                totalChangeRequestLMR.add(new TcChangeRequestRevision(session, itemRev));
            }
        }
    }

    /**
     * Retrieve the ProductRevision for the R20 report.
     *
     * @param session an active teamcenter session
     * @param report  will provide the informations of the report which is executing
     * @return the built Product Revision
     * @throws NotFoundException if no revisions of the product Before could be found
     */
    ProductRevision retrieveProductRevisionBefore(final TcSession session, final ExecutionR20 report) throws NotFoundException {
        ItemRevisionQuery queryProductBefore = new ItemRevisionQuery(session);
        queryProductBefore.searchID(report.getProductUnitBefore().getProductID());
        List<ItemRevision> revisionsProductBefore = queryProductBefore.findAll();
        if (revisionsProductBefore.isEmpty()) {
            throw new NotFoundException("Could not find the requested product before");
        }
        return new ProductRevision(session, revisionsProductBefore.get(0));
    }

    private void retrieveChangeRequests(final UnitRevision unitAfter) {
        for (TcChangeRequestRevision change : unitAfter.getChangeRequestsList()) {
            if (change.isCadOrDCadOrMace()) { // filter: only CAD , DCAD or MACE
                totalChangeRequestNotLMR.add(change);
            }
        }
        totalChangeRequest.addAll(totalChangeRequestLMR);
        totalChangeRequest.addAll(totalChangeRequestNotLMR);
    }

    /**
     * Retrieve the list of all the Generics in the pseudo folders of each ChangeNotice of each Removed Modification.
     *
     * @param unitBefore the unit before
     * @param unitAfter  the unit after
     */
    private void retrieveAllGenerics(final UnitRevision unitBefore, final UnitRevision unitAfter) {
        // FIRST GENERICS BEFORE **********************************************************************************
        // Gather all generics before, make them point to an empty list of mods, which will be enriched later as needed with LMR
        for (AbstractGeneric generic : unitBefore.getGenerics()) {
            if (GENERIC.equals(generic.getType())) { // Technical view only
                totalGenericsBefore.put(generic, new TreeSet<>(CHANGE_COMPARATOR));
            }
        }
        totalGenericsBeforeNoMod.addAll(totalGenericsBefore.keySet()); // Preventively add them all the generics here, when we see they undergo a change, remove them from here

        // Retrieve the list of all the generics in the pseudo folders of each ChangeNotices of the Unit Before.
        retrieveGenericsOfUnitBefore();

        // NOW GENERICS AFTER **********************************************************************************

        // Gather all generics after, make them point to an empty list of mods, which will be enriched later as needed with some Changes after
        for (AbstractGeneric generic : unitAfter.getGenerics()) {
            if (GENERIC.equals(generic.getType())) { // Technical view only
                totalGenericsAfter.put(generic, new TreeSet<>(CHANGE_COMPARATOR));
            }
        }
        totalGenericsAfterNoMod.addAll(totalGenericsAfter.keySet()); // Preventively add them all, when we see they undergo a change, remove them from here

        // Retrieve the list of all the generics in the pseudo folders of each ChangeNotices of the Unit After.
        retrieveGenericsOfUnitAfter();

        // Gather all generics ever encountered ***********************************************************
        totalGenerics.addAll(totalGenericsBefore.keySet());
        totalGenerics.addAll(totalGenericsAfter.keySet());
    }

    private void retrieveGenericsOfUnitBefore() {
        for (TcChangeRequestRevision changeRequest : totalChangeRequestLMR) {
            if (changeRequest.isCadOrDCadOrMace()) {
                for (TcChangeNoticeRevision changeNotice : changeRequest.getChangeNoticesList()) {
                    if (changeNotice.isEtude()) { // Only keep change Notice "ETUDE" !
                        extractGenericsFromChangeNotice(changeRequest, changeNotice);
                    }
                }
            }
        }
        totalGenericsBeforeLMR.putAll(totalGenericsBeforeLMRWithRupture);
        totalGenericsBeforeLMR.putAll(totalGenericsBeforeLMRWithoutRupture);
    }

    /**
     * Retrieve the generic for the report R20.
     */
    private void retrieveGenericsOfUnitAfter() {
        for (TcChangeRequestRevision changeRequest : totalChangeRequestNotLMR) {
            for (TcChangeNoticeRevision changeNotice : changeRequest.getChangeNoticesList()) {
                for (AbstractGeneric genericWithRupture : changeNotice.getListOfGenericRevWithRupture()) {
                    if (GENERIC.equals(genericWithRupture.getType())) { // Technical view only
                        Set<TcChangeRequestRevision> setOfModWithRupture = totalGenericsAfterWithRupture.get(genericWithRupture);
                        if (setOfModWithRupture == null) {
                            setOfModWithRupture = new TreeSet<>(CHANGE_COMPARATOR);
                            totalGenericsAfterWithRupture.put(genericWithRupture, setOfModWithRupture);
                        }
                        setOfModWithRupture.add(changeRequest);
                        totalGenericsAfterNoMod.remove(genericWithRupture);
                    }
                }
                for (AbstractGeneric genericWithoutRupture : changeNotice.getListOfGenericRevWithoutRupture()) {
                    if (GENERIC.equals(genericWithoutRupture.getType())) { // Technical view only
                        Set<TcChangeRequestRevision> setOfModWithoutRupture = totalGenericsAfterWithoutRupture.get(genericWithoutRupture);
                        if (setOfModWithoutRupture == null) {
                            setOfModWithoutRupture = new TreeSet<>(CHANGE_COMPARATOR);
                            totalGenericsAfterWithoutRupture.put(genericWithoutRupture, setOfModWithoutRupture);
                        }
                        setOfModWithoutRupture.add(changeRequest);
                        totalGenericsAfterNoMod.remove(genericWithoutRupture);
                    }
                }
            }
        }
    }

    /**
     * Adds the provided Change Notice's Generics with and withour rupture to the list of Generics LMR (IF they also are mentionned in the unit Before).<br>
     * By the end og this method:
     * <ul>
     * <li>{@code totalGenericsBeforeLMRWithRupture} will contain, for each GenericBefore concerned with this LMR, a link to the CR</li>
     * <li></li>
     * <li></li>
     * <li></li>
     * </ul>
     */
    private void extractGenericsFromChangeNotice(final TcChangeRequestRevision changeRequest, final TcChangeNoticeRevision changeNotice) {

        for (AbstractGeneric genericWithOrWithoutRupture : changeNotice.getListOfGenericRevWithOrWithoutRupture()) {

            // A change notice 'Etude' may still contain Generic Assembs, we don't want them
            // also, a Generic contained in an LMR than has nothing to do with the Unit Before, must be ignored
            if (GENERIC.equals(genericWithOrWithoutRupture.getType()) && totalGenericsBefore.containsKey(genericWithOrWithoutRupture)) {
                Set<TcChangeRequestRevision> setOfModWithRupture = totalGenericsBeforeLMRWithRupture.get(genericWithOrWithoutRupture);
                if (setOfModWithRupture == null) {
                    setOfModWithRupture = new TreeSet<>(CHANGE_COMPARATOR);
                    totalGenericsBeforeLMRWithRupture.put(genericWithOrWithoutRupture, setOfModWithRupture);
                }
                setOfModWithRupture.add(changeRequest);
                totalGenericsBeforeNoMod.remove(genericWithOrWithoutRupture);
            }
        }
        for (AbstractGeneric genericWithoutRupture : changeNotice.getListOfGenericRevWithoutRupture()) {
            // A change notice 'Etude' may still contain Generic Assembs, we don't want them
            // also, a Generic contained in an LMR than has nothing to do with the Unit Before, must be ignored
            if (GENERIC.equals(genericWithoutRupture.getType()) && totalGenericsBefore.containsKey(genericWithoutRupture)) {
                Set<TcChangeRequestRevision> setOfModWithoutRupture = totalGenericsBeforeLMRWithoutRupture.get(genericWithoutRupture);
                if (setOfModWithoutRupture == null) {
                    setOfModWithoutRupture = new TreeSet<>(CHANGE_COMPARATOR);
                    totalGenericsBeforeLMRWithRupture.put(genericWithoutRupture, setOfModWithoutRupture);
                }
                setOfModWithoutRupture.add(changeRequest);
                totalGenericsBeforeNoMod.remove(genericWithoutRupture);
            }
        }
    }
}
