/*******************************************************************************
 * Copyright (C) 2021, 1C-Soft LLC and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     1C-Soft LLC - initial API and implementation
 *******************************************************************************/
package com.e1c.v8codestyle.bsl.check;

import static com._1c.g5.v8.dt.bsl.model.BslPackage.Literals.MODULE;

import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.xtext.EcoreUtil2;
import org.eclipse.xtext.nodemodel.INode;

import com._1c.g5.v8.dt.bsl.common.VariableProcessor;
import com._1c.g5.v8.dt.bsl.model.Block;
import com._1c.g5.v8.dt.bsl.model.BslPackage;
import com._1c.g5.v8.dt.bsl.model.DeclareStatement;
import com._1c.g5.v8.dt.bsl.model.ExplicitVariable;
import com._1c.g5.v8.dt.bsl.model.ForToStatement;
import com._1c.g5.v8.dt.bsl.model.FormalParam;
import com._1c.g5.v8.dt.bsl.model.ImplicitVariable;
import com._1c.g5.v8.dt.bsl.model.Method;
import com._1c.g5.v8.dt.bsl.model.Module;
import com._1c.g5.v8.dt.bsl.model.SimpleStatement;
import com._1c.g5.v8.dt.bsl.model.StaticFeatureAccess;
import com._1c.g5.v8.dt.bsl.model.Variable;
import com._1c.g5.v8.dt.bsl.model.resource.owner.IBslOwnerComputerService;
import com._1c.g5.v8.dt.bsl.resource.BslResource;
import com._1c.g5.v8.dt.bsl.resource.DynamicFeatureAccessComputer;
import com._1c.g5.v8.dt.bsl.typesystem.util.TypeSystemUtil;
import com.e1c.g5.v8.dt.check.ICheckParameters;
import com.e1c.g5.v8.dt.check.components.BasicCheck;
import com.e1c.g5.v8.dt.check.settings.IssueType;
import com.e1c.v8codestyle.check.CommonSenseCheckExtension;
import com.e1c.v8codestyle.internal.bsl.BslPlugin;
import com.google.common.collect.Lists;
import com.google.inject.Inject;

/**
 * Unused module local variable check.
 *
 * @author Andrey Volkov
 */
public final class ModuleUnusedLocalVariableCheck
    extends BasicCheck
{
    private static final String CHECK_ID = "module-unused-local-variable"; //$NON-NLS-1$

    private final DynamicFeatureAccessComputer dynamicComputer;

    private final IBslOwnerComputerService ownerService;

    /**
     * Instantiates a new module unused local variable check.
     *
     * @param dynamicComputer the dynamic computer service, cannot be {@code null}
     * @param ownerService the owner service service, cannot be {@code null}
     */
    @Inject
    public ModuleUnusedLocalVariableCheck(DynamicFeatureAccessComputer dynamicComputer,
        IBslOwnerComputerService ownerService)
    {
        super();

        this.dynamicComputer = dynamicComputer;
        this.ownerService = ownerService;
    }

    @Override
    public String getCheckId()
    {
        return CHECK_ID;
    }

    @Override
    protected void configureCheck(CheckConfigurer builder)
    {
        builder.title(Messages.ModuleUnusedLocalVariableCheck_Title)
            .description(Messages.ModuleUnusedLocalVariableCheck_Description)
            .issueType(IssueType.WARNING)
            .extension(new CommonSenseCheckExtension(getCheckId(), BslPlugin.PLUGIN_ID))
            .module()
            .checkedObjectType(MODULE);
    }

    @Override
    protected void check(Object object, ResultAcceptor resultAceptor, ICheckParameters parameters,
        IProgressMonitor progressMonitor)
    {
        Module module = (Module)object;

        VariableProcessor processor = new VariableProcessor(module.allStatements(), dynamicComputer);
        processor.process();
        Map<Variable, List<INode>> variablesChangeValueModule = processor.variablesForChangeValue();
        Map<Variable, List<INode>> variablesForReadValueModule = processor.variablesForReadValue();
        for (Method method : getProcessMethods(module))
        {
            processor = new VariableProcessor(method.allStatements(), dynamicComputer);
            processor.process();
            Map<Variable, List<INode>> variablesChangeValue = processor.variablesForChangeValue();
            Map<Variable, List<INode>> variablesForReadValue = processor.variablesForReadValue();
            for (Map.Entry<Variable, List<INode>> variable : variablesChangeValue.entrySet())
            {
                if (variable.getKey() instanceof FormalParam)
                {
                    continue;
                }
                Block actualVariableBlock = EcoreUtil2.getContainerOfType(variable.getKey(), Block.class);
                if (actualVariableBlock != method)
                {
                    continue;
                }
                if (variable.getKey() instanceof ExplicitVariable && ((ExplicitVariable)variable.getKey()).isExport())
                {
                    continue;
                }
                if (variable.getKey() instanceof ImplicitVariable)
                {
                    ImplicitVariable implicitVariable = (ImplicitVariable)variable.getKey();
                    if (implicitVariable.eContainer()
                        .eContainingFeature() == BslPackage.Literals.FOR_STATEMENT__VARIABLE_ACCESS
                        && implicitVariable.eContainer().eContainer() instanceof ForToStatement)
                    {
                        continue;
                    }
                    if (isUniqueForStaticFeatureAccess(implicitVariable))
                    {
                        continue;
                    }
                }
                if (actualVariableBlock == module)
                {
                    variablesChangeValueModule.computeIfAbsent(variable.getKey(), item -> Lists.newArrayList())
                        .addAll(variable.getValue());
                }
                List<INode> readValueNodes = variablesForReadValue.get(variable.getKey());
                if (readValueNodes == null)
                {
                    resultAceptor
                        .addIssue(MessageFormat.format(Messages.ModuleUnusedLocalVariableCheck_Unused_local_variable__0,
                            variable.getKey().getName()), variable.getKey());
                }
                else if (variable.getKey() instanceof ImplicitVariable)
                {
                    ImplicitVariable implicitVariable = (ImplicitVariable)variable.getKey();
                    EObject variableOwner = implicitVariable.eContainer();
                    if (variableOwner instanceof StaticFeatureAccess
                        && variableOwner.eContainer() instanceof SimpleStatement)
                    {
                        for (INode readValueNode : readValueNodes)
                        {
                            if (readValueNode.getSemanticElement() == null)
                            {
                                continue;
                            }
                            if (EcoreUtil2.getContainerOfType(readValueNode.getSemanticElement(),
                                SimpleStatement.class) == variableOwner.eContainer())
                            {
                                resultAceptor.addIssue(MessageFormat.format(
                                    Messages.ModuleUnusedLocalVariableCheck_Probably_variable_not_initilized_yet__0,
                                    implicitVariable.getName()), readValueNode.getSemanticElement());
                            }
                        }
                    }
                }
            }
            for (Map.Entry<Variable, List<INode>> variable : variablesForReadValue.entrySet())
            {
                Block actualVariableBlock = EcoreUtil2.getContainerOfType(variable.getKey(), Block.class);
                if (actualVariableBlock == module)
                {
                    variablesForReadValueModule.computeIfAbsent(variable.getKey(), item -> Lists.newArrayList())
                        .addAll(variable.getValue());
                }
            }
            for (DeclareStatement decl : method.allDeclareStatements())
            {
                for (ExplicitVariable variable : decl.getVariables())
                {
                    if (variable.isExport())
                    {
                        continue;
                    }
                    boolean foundInChange = variablesChangeValue.keySet().contains(variable);
                    boolean foundInRead = variablesForReadValue.keySet().contains(variable);
                    if (!foundInChange && !foundInRead)
                    {
                        resultAceptor.addIssue(
                            MessageFormat.format(Messages.ModuleUnusedLocalVariableCheck_Unused_local_variable__0,
                                variable.getName()),
                            variable);
                    }
                    if (!foundInChange)
                    {
                        resultAceptor.addIssue(MessageFormat.format(
                            Messages.ModuleUnusedLocalVariableCheck_Probably_variable_not_initilized_yet__0,
                            variable.getName()), variable);
                    }
                    if (foundInChange && foundInRead)
                    {
                        INode changeValueNode = variablesChangeValue.get(variable).get(0);
                        INode readValueNode = variablesForReadValue.get(variable).get(0);
                        if (readValueNode.getOffset() < changeValueNode.getOffset())
                        {
                            resultAceptor.addIssue(MessageFormat.format(
                                Messages.ModuleUnusedLocalVariableCheck_Probably_variable_not_initilized_yet__0,
                                variable.getName()), readValueNode.getSemanticElement());
                        }
                    }
                }
            }
        }
        for (Map.Entry<Variable, List<INode>> variable : variablesChangeValueModule.entrySet())
        {
            if (EcoreUtil2.getContainerOfType(variable.getKey(), Block.class) != module)
            {
                continue;
            }
            if (variable.getKey() instanceof ExplicitVariable && ((ExplicitVariable)variable.getKey()).isExport())
            {
                continue;
            }
            List<INode> readValueNodes = variablesForReadValueModule.get(variable.getKey());
            if (readValueNodes == null && !(variable.getKey() instanceof ImplicitVariable
                && isUniqueForStaticFeatureAccess((ImplicitVariable)variable.getKey())))
            {
                resultAceptor
                    .addIssue(MessageFormat.format(Messages.ModuleUnusedLocalVariableCheck_Unused_local_variable__0,
                        variable.getKey().getName()), variable.getKey());
            }
        }
    }

    private List<Method> getProcessMethods(Module module)
    {
        List<Method> processingObjects;
        if (!onlyMethodReparse(module))
        {
            processingObjects = Lists.newArrayList(module.allMethods());
        }
        else
        {
            BslResource resource = (BslResource)module.eResource();
            processingObjects = com._1c.g5.v8.dt.bsl.util.BslUtil.getDependMethodsTree(resource.getReparseMethod())
                .stream()
                .filter(item -> item instanceof Method)
                .map(item -> (Method)item)
                .collect(Collectors.toList());
            processingObjects.add(resource.getReparseMethod());
        }
        return processingObjects;
    }

    private boolean onlyMethodReparse(Module module)
    {
        // TODO - only full validation first, optimization later
        //return module.eResource() != null && ((BslResource)module.eResource()).isOnlyMethodReparse()
        //    && getCheckMode() != BslCancelableDiagnostician.ALL_FOR_FULL_MODULE;
        return false;
    }

    // TODO - only full validation first, optimization later
    //private CheckMode getCheckMode()
    //{
    //    return state.get().checkMode;
    //}

    private boolean isUniqueForStaticFeatureAccess(ImplicitVariable implicitVariable)
    {
        return implicitVariable.eContainer() instanceof StaticFeatureAccess && !implicitVariable.getEnvironments()
            .equals(TypeSystemUtil.getActualEnvironments(implicitVariable.eContainer(), ownerService));
    }

}
