/*
 * Copyright 2019 junichi11.
 *
 * Licensed 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.netbeans.modules.php.cake3.editor.visitors;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.netbeans.modules.php.api.editor.EditorSupport;
import org.netbeans.modules.php.api.editor.PhpBaseElement;
import org.netbeans.modules.php.api.editor.PhpClass;
import org.netbeans.modules.php.api.phpmodule.PhpModule;
import org.netbeans.modules.php.api.util.StringUtils;
import org.netbeans.modules.php.cake3.modules.CakePHP3Module.Category;
import org.netbeans.modules.php.cake3.utils.CakePHPCodeUtils;
import org.netbeans.modules.php.editor.CodeUtils;
import org.netbeans.modules.php.editor.parser.astnodes.Assignment;
import org.netbeans.modules.php.editor.parser.astnodes.ClassDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.Expression;
import org.netbeans.modules.php.editor.parser.astnodes.ExpressionStatement;
import org.netbeans.modules.php.editor.parser.astnodes.FieldAccess;
import org.netbeans.modules.php.editor.parser.astnodes.FunctionDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.FunctionInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.MethodDeclaration;
import org.netbeans.modules.php.editor.parser.astnodes.MethodInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.NamespaceName;
import org.netbeans.modules.php.editor.parser.astnodes.Scalar;
import org.netbeans.modules.php.editor.parser.astnodes.StaticMethodInvocation;
import org.netbeans.modules.php.editor.parser.astnodes.Variable;
import org.netbeans.modules.php.editor.parser.astnodes.VariableBase;
import org.openide.filesystems.FileObject;
import org.openide.util.Lookup;
import org.openide.util.Pair;

public class ControllerVisitor extends FieldsVisitor {

    private static final String LOAD_COMPONENT_METHOD = "loadComponent"; // NOI18N
    private static final String LOAD_MODEL_METHOD = "loadModel"; // NOI18N
    private static final String TABLE_REGISTRY_METHOD = "TableRegistry"; // NOI18N
    private static final String RENDER_METHOD = "render"; // NOI18N
    private static final String SET_METHOD = "set"; // NOI18N
    private final boolean isTemplate;
    private String currentMethodName = ""; // NOI18N
    private String templateName = ""; // NOI18N
    private final Set<String> templateNames = Collections.synchronizedSet(new HashSet<>());
    private final Set<String> allTemplateNames = Collections.synchronizedSet(new HashSet<>());
    private final Set<String> themeNames = Collections.synchronizedSet(new HashSet<>());
    private final Set<String> allThemeNames = Collections.synchronizedSet(new HashSet<>());

    public ControllerVisitor(PhpModule phpModule) {
        this(phpModule, false);
    }

    public ControllerVisitor(PhpModule phpModule, boolean isTemplate) {
        super(phpModule);
        this.isTemplate = isTemplate;
    }

    public ControllerVisitor(PhpModule phpModule, FileObject targetFile, boolean isTemplate, int caretPosition) {
        super(phpModule);
        this.isTemplate = isTemplate;
        EditorSupport editorSupport = Lookup.getDefault().lookup(EditorSupport.class);
        PhpBaseElement phpElement = editorSupport.getElement(targetFile, caretPosition);
        if (phpElement != null && phpElement instanceof PhpClass.Method) {
            PhpClass.Method method = (PhpClass.Method) phpElement;
            templateName = method.getName();
            allTemplateNames.add(templateName);
        }
    }

    @Override
    public Set<String> getAvailableFieldNames() {
        if (isTemplate) {
            return new HashSet<>(Arrays.asList(HELPERS));
        }
        return new HashSet<>(Arrays.asList(COMPONENTS, MODELS));
    }

    @Override
    public void visit(ExpressionStatement node) {
        super.visit(node);
        if (StringUtils.isEmpty(currentMethodName)) {
            return;
        }

        Expression expression = node.getExpression();
        if (expression instanceof Assignment) {
            Assignment assignment = (Assignment) expression;
            Assignment.Type operator = assignment.getOperator();
            if (operator != Assignment.Type.EQUAL) {
                return;
            }

            VariableBase leftHandSide = assignment.getLeftHandSide();
            if (leftHandSide instanceof FieldAccess) {
                // e.g. $this->theme = 'Modern';
                // left
                FieldAccess f = (FieldAccess) leftHandSide;
                Variable v = f.getField();
                String variableName = CodeUtils.extractVariableName(v);
                if (!"theme".equals(variableName)) { // NOI18N
                    return;
                }

                // right
                Expression rightHandSide = assignment.getRightHandSide();
                String rightValue = CakePHPCodeUtils.getStringValue(rightHandSide);
                if (rightValue.isEmpty()) {
                    return;
                }

                if (currentMethodName.equals(templateName)) {
                    themeNames.add(rightValue);
                } else {
                    allThemeNames.add(rightValue);
                }
            }
        }
    }

    @Override
    public void visit(ClassDeclaration node) {
        // add a default table e.g. UsersController -> UsersTable
        String className = CodeUtils.extractClassName(node);
        int lastIndexOfController = className.lastIndexOf(Category.CONTROLLER.getSuffix());
        if (lastIndexOfController > 0) {
            String controllerName = className.substring(0, lastIndexOfController);
            Pair<String, PhpClass> phpClass = createPhpClass(Category.TABLE, controllerName, controllerName);
            if (phpClass != null) {
                addPhpClasss(Category.TABLE, phpClass);
            }
        }
        super.visit(node);
    }

    @Override
    public void visit(MethodDeclaration node) {
        FunctionDeclaration function = node.getFunction();
        currentMethodName = CodeUtils.extractFunctionName(function);
        if (templateName.equals(currentMethodName)) {
            templateNames.add(currentMethodName);
        }
        if (!currentMethodName.startsWith("_")) { // NOI18N
            // XXX ignore override methods? e.g. initialize
            allTemplateNames.add(currentMethodName);
        }
        super.visit(node);
    }

    @Override
    public void visit(MethodInvocation node) {
        FunctionInvocation method = node.getMethod();
        handleMethod(method);
        super.visit(node);
    }

    @Override
    public void visit(StaticMethodInvocation node) {
        super.visit(node);
        // TableRegistry::get('Users');
        String methodClassName = getClassName(node);
        if (methodClassName == null || !TABLE_REGISTRY_METHOD.equals(methodClassName)) {
            return;
        }
        FunctionInvocation method = node.getMethod();
        String methodName = CodeUtils.extractFunctionName(method);
        if (!methodName.equals("get")) { // NOI18N
            return;
        }
        addModel(method);
    }

    private String getClassName(StaticMethodInvocation node) {
        Expression className = node.getDispatcher();
        if (className instanceof NamespaceName) {
            return CodeUtils.extractQualifiedName((NamespaceName) className);
        }
        return null;
    }

    private void handleMethod(FunctionInvocation method) {
        String name = CodeUtils.extractFunctionName(method);
        if (StringUtils.isEmpty(name)) {
            return;
        }
        switch (name) {
            case LOAD_COMPONENT_METHOD:
                addComponent(method);
                break;
            case LOAD_MODEL_METHOD:
                addModel(method);
                break;
            case RENDER_METHOD:
                addTemplate(method);
                break;
            case SET_METHOD:
                // TODO
                break;
            default:
                break;
        }
    }

    private void addComponent(FunctionInvocation method) {
        List<Expression> parameters = method.getParameters();
        Pair<String, String> aliasAndEntityName = CakePHPCodeUtils.getAliasAndEntityName(parameters);
        String aliasName = aliasAndEntityName.first();
        String entityName = aliasAndEntityName.second();
        if (!StringUtils.isEmpty(aliasName)) {
            if (StringUtils.isEmpty(entityName)) {
                entityName = aliasName;
            }
            Pair<String, PhpClass> phpClass = createPhpClass(Category.COMPONENT, aliasName, entityName);
            if (phpClass != null) {
                addPhpClasss(Category.COMPONENT, phpClass);
            }
        }
    }

    private void addModel(FunctionInvocation method) {
        List<Expression> parameters = method.getParameters();
        String tableName = ""; // NOI18N
        for (Expression parameter : parameters) {
            if (parameter instanceof Scalar) {
                tableName = CakePHPCodeUtils.getStringValue(parameter);
            }
            // only the first parameter
            break;
        }
        if (!StringUtils.isEmpty(tableName)) {
            Pair<String, PhpClass> phpClass = createPhpClass(Category.TABLE, tableName, ""); // NOI18N
            if (phpClass != null) {
                addPhpClasss(Category.TABLE, phpClass);
            }
        }
    }

    private void addTemplate(FunctionInvocation method) {
        List<Expression> parameters = method.getParameters();
        String methodName = ""; // NOI18N
        for (Expression parameter : parameters) {
            if (parameter instanceof Scalar) {
                methodName = CakePHPCodeUtils.getStringValue(parameter);
            }
            // only the first parameter
            // XXX add layout?
            break;
        }
        if (currentMethodName.equals(templateName)) {
            templateNames.add(methodName);
        }
        if (!StringUtils.isEmpty(methodName)) {
            allTemplateNames.add(methodName);
        }
    }

    public List<String> getTemplateNames() {
        return new ArrayList<>(templateNames);
    }

    public List<String> getAllTemplateNames() {
        return new ArrayList<>(allTemplateNames);
    }

    public List<String> getThemeNames() {
        return new ArrayList<>(themeNames);
    }

    public List<String> getAllThemeNames() {
        return new ArrayList<>(allThemeNames);
    }
}