package fr.adrienbrault.idea.symfony2plugin;

import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiElement;
import com.jetbrains.php.lang.psi.elements.Method;
import com.jetbrains.php.lang.psi.elements.MethodReference;
import com.jetbrains.php.lang.psi.elements.StringLiteralExpression;
import com.jetbrains.php.lang.psi.resolve.types.PhpType;
import com.jetbrains.php.lang.psi.resolve.types.PhpTypeProvider;
import org.jetbrains.annotations.Nullable;
import org.xml.sax.SAXException;

import javax.xml.parsers.*;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author Adrien Brault <adrien.brault@gmail.com>
 */
public class SymfonyContainerTypeProvider implements PhpTypeProvider {

    private Map<String, Map<String, String>> servicesMapsPerProject;
    private Map<String, Long> servicesMapsLastModifiedPerProject;

    public SymfonyContainerTypeProvider() {
        servicesMapsPerProject = new HashMap<String, Map<String, String>>();
        servicesMapsLastModifiedPerProject = new HashMap<String, Long>();
    }

    @Nullable
    @Override
    public PhpType getType(PsiElement e) {
        if (DumbService.getInstance(e.getProject()).isDumb()) {
            return null;
        }

        if (!isContainerGetCall(e)) {
            return null;
        }

        String serviceId = getServiceId((MethodReference) e);
        if (null == serviceId) {
            return null;
        }

        Map<String, String> serviceMap = getServicesMap(e.getProject());
        String serviceClass = serviceMap.get(serviceId);

        if (null == serviceClass) {
            return null;
        }

        return new PhpType().add(serviceClass);
    }

    private boolean isContainerGetCall(PsiElement e) {
        if (!(e instanceof  MethodReference)) {
            return false;
        }

        MethodReference methodRef = (MethodReference) e;
        if (null == e.getReference()) {
            return false;
        }

        PsiElement resolvedReference = methodRef.getReference().resolve();
        if (!(resolvedReference instanceof Method)) {
            return false;
        }

        Method method = (Method) resolvedReference;
        String methodFQN = method.getFQN(); // Something like "\Symfony\Bundle\FrameworkBundle\Controller\Controller.get"
        if (null == methodFQN) {
            return false;
        }

        if (methodFQN.equals("\\Symfony\\Bundle\\FrameworkBundle\\Controller\\Controller.get")
            || methodFQN.equals("\\Symfony\\Component\\DependencyInjection\\ContainerInterface.get")) {
            return true;
        }

        return false;
    }

    private String getServiceId(MethodReference e) {
        String serviceId = null;

        PsiElement[] parameters = e.getParameters();
        if (parameters.length > 0 && parameters[0] instanceof StringLiteralExpression) {
            serviceId = parameters[0].getText(); // quoted string
            serviceId = serviceId.substring(1, serviceId.length() - 1);
        }

        return serviceId;
    }

    private Map<String, String>getServicesMap(Project project) {
        Map<String, String> map = new HashMap<String, String>();

        String defaultServiceMapFilePath = project.getBasePath() + "/" + Settings.getInstance(project).pathToProjectContainer;
        File xmlFile = new File(defaultServiceMapFilePath);
        if (!xmlFile.exists()) {
            return map;
        }

        Long xmlFileLastModified = xmlFile.lastModified();
        if (xmlFileLastModified.equals(servicesMapsLastModifiedPerProject.get(project.getBasePath()))) {
            return servicesMapsPerProject.get(project.getBasePath());
        }

        try {
            ServiceMapParser serviceMapParser = new ServiceMapParser();
            map = serviceMapParser.parse(xmlFile);
            servicesMapsPerProject.put(project.getBasePath(), map);
            servicesMapsLastModifiedPerProject.put(project.getBasePath(), xmlFileLastModified);
        } catch (SAXException e) {
            return map;
        } catch (IOException e) {
            return map;
        } catch (ParserConfigurationException e) {
            return map;
        }

        return map;
    }

}

@@ -169,16 +169,178 @@
 lement;%0A
+import com.jetbrains.php.codeInsight.controlFlow.instructions.PhpInstruction;%0Aimport com.jetbrains.php.codeInsight.controlFlow.instructions.PhpReturnInstruction;%0A
 import c
@@ -382,16 +382,16 @@
 Method;%0A
-
 import c
@@ -1985,32 +1985,355 @@
 PsiElement e) %7B%0A
+        return isContainerGetCall(e, 1);%0A    %7D%0A%0A    private boolean isContainerGetCall(PsiElement e, int deepness) %7B%0A        // ((Method)((MethodReference)((PhpReturnInstruction)((Method)((MethodReference)e).getReference().resolve()).getControlFlow().getInstructions()%5B6%5D).getArgument()).getReference().resolve()).getFQN()%0A
         if (!(e 
@@ -2957,223 +2957,1204 @@
     
-if (methodFQN.equals(%22%5C%5CSymfony%5C%5CBundle%5C%5CFrameworkBundle%5C%5CController%5C%5CController.get%22)%0A            %7C%7C methodFQN.equals(%22%5C%5CSymfony%5C%5CComponent%5C%5CDependencyInjection%5C%5CContainerInterface.get%22)) %7B%0A            return true;
+String expectedMethodFQN = %22%5C%5CSymfony%5C%5CComponent%5C%5CDependencyInjection%5C%5CContainerInterface.get%22;%0A        if (methodFQN.equals(expectedMethodFQN)) %7B%0A            return true;%0A        %7D%0A%0A        if (deepness %3E 3) %7B%0A            return false;%0A        %7D%0A%0A        // Try to see if this method return expression is a method call to a ContainerInterface::get ... recursive!%0A%0A        PhpInstruction%5B%5D instructions = method.getControlFlow().getInstructions();%0A        for (int i = 0; i %3C instructions.length; i++) %7B%0A            PhpInstruction instruction = instructions%5Bi%5D;%0A%0A            if (instruction instanceof PhpReturnInstruction) %7B%0A                PhpReturnInstruction returnInstruction = (PhpReturnInstruction) instruction;%0A%0A                PsiElement returnInstructionElement = returnInstruction.getArgument();%0A                if (null != returnInstructionElement &&%0A                    null != returnInstructionElement.getReference() &&%0A                    returnInstructionElement.getReference().resolve() != resolvedReference) %7B // Avoid stackoverflow with method calling itself%0A                    return isContainerGetCall(returnInstructionElement, deepness + 1);%0A                %7D%0A            %7D
 %0A   

Support all ContainerInterface::get proxy as long as the id is the first argument