/*
 * Copyright 2005 Sascha Weinreuter
 *
 * 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.intellij.lang.xpath.xslt.psi.impl;

import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.navigation.ItemPresentation;
import com.intellij.navigation.NavigationItem;
import com.intellij.navigation.PsiElementNavigationItem;
import com.intellij.openapi.util.Iconable;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiReference;
import com.intellij.psi.impl.light.LightElement;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import org.intellij.lang.xpath.completion.CompletionLists;
import org.intellij.lang.xpath.context.ContextProvider;
import org.intellij.lang.xpath.context.XPathVersion;
import org.intellij.lang.xpath.psi.XPathElement;
import org.intellij.lang.xpath.psi.XPathElementVisitor;
import org.intellij.lang.xpath.xslt.XsltSupport;
import org.intellij.lang.xpath.xslt.psi.XsltElement;
import org.intellij.lang.xpath.xslt.psi.XsltElementFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

abstract class XsltElementImpl extends LightElement implements Iconable, PsiElementNavigationItem, XsltElement, ItemPresentation {
    protected final @NotNull XmlTag myElement;
    protected final XsltElementFactory myElementFactory;

    private final int myHashCode;
    private PsiElement myNavigationElement;

    protected XsltElementImpl(XmlTag target) {
        super(target.getManager(), XsltLanguage.INSTANCE);
        myElement = target;
        myHashCode = myElement.hashCode();
        myElementFactory = XsltElementFactory.getInstance();
    }

    @Override
    public PsiElement copy() {
        return myElementFactory.wrapElement((XmlTag)myElement.copy(), getClass());
    }

    @Override
    public String getText() {
        return myElement.getText();
    }

    @Override
    public XmlTag getTag() {
        return myElement;
    }

    @Override
    public final @NotNull ItemPresentation getPresentation() {
        return this;
    }

    @Override
    public @Nullable Icon getIcon(boolean open) {
        return getIcon(0);
    }

    @Override
    public @Nullable String getLocationString() {
        return "(in " + getContainingFile().getName() + ")";
    }

    @Override
    public String getPresentableText() {
        return getName();
    }

    @Override
    public PsiElement getTargetElement() {
        return myElement;
    }

    @Override
    public @Nullable String getName() {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.getValue() : null;
    }

    public PsiElement setName(@NotNull String name) throws IncorrectOperationException {
        assert myElement.isValid();

        myElement.setAttribute("name", name);
        return this;
    }

    @Override
    @SuppressWarnings("RawUseOfParameterizedType")
    public @NotNull PsiElement getNavigationElement() {
        if (myNavigationElement == null && myElement.isValid()) {
            final Class[] allInterfaces = CompletionLists.getAllInterfaces(myElement.getClass());
            myNavigationElement = (PsiElement)Proxy.newProxyInstance(getClass().getClassLoader(), allInterfaces, new InvocationHandler() {
                @Override
                @SuppressWarnings({"AutoBoxing", "AutoUnboxing"})
                public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    try {
                        final XmlAttributeValue nameElement = XsltElementImpl.this.getNameElement();
                        return switch (method.getName()) {
                            case "navigate" -> {
                              assert nameElement != null;
                              ((NavigationItem)nameElement).navigate((Boolean)args[0]);
                              yield null;
                            }
                            case "canNavigate" -> nameElement instanceof NavigationItem item && item.canNavigate();
                            case "getTextOffset" -> nameElement != null ? nameElement.getTextOffset() : myElement.getTextOffset();
                            default -> method.invoke(myElement, args);
                        };
                    } catch (InvocationTargetException e1) {
                        throw e1.getTargetException();
                    }
                }
            });
        }
        return myElement.isValid() ? myNavigationElement : this;
    }

    public @Nullable XmlAttribute getNameAttribute() {
        return myElement.getAttribute("name", null);
    }

    public @Nullable PsiElement getNameIdentifier() {
        final XmlAttribute nameAttribute = getNameAttribute();
        return nameAttribute != null ? XsltSupport.getAttValueToken(nameAttribute) : null;
    }

    private @Nullable XmlAttributeValue getNameElement() {
        XmlAttribute attribute = getNameAttribute();
        return attribute != null ? attribute.getValueElement() : null;
    }

    @Override
    public PsiElement getOriginalElement() {
        return myElement.getOriginalElement();
    }

    @Override
    public boolean isValid() {
        return myElement.isValid();
    }

    @Override
    public @NotNull Language getLanguage() {
        return XsltLanguage.INSTANCE;
    }

    @Override
    public void navigate(boolean b) {
        final XmlAttributeValue nameElement = getNameElement();
        assert nameElement != null;
        ((NavigationItem)nameElement).navigate(b);
    }

    @Override
    public boolean canNavigate() {
        final XmlAttributeValue nameElement = getNameElement();
        return myElement.isValid() && nameElement instanceof NavigationItem && ((NavigationItem)nameElement).canNavigate();
    }

    @Override
    public boolean canNavigateToSource() {
        return canNavigate();
    }

    public void accept(@NotNull XPathElementVisitor visitor) {
      visitor.visitXPathElement((XPathElement)this);
    }

    @Override
    public void accept(@NotNull PsiElementVisitor visitor) {
      if (visitor instanceof XPathElementVisitor && this instanceof XPathElement) {
        accept((XPathElementVisitor)visitor);
      } else {
        myElement.accept(visitor);
      }
    }

    @Override
    public int hashCode() {
        return myHashCode;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        final XsltElementImpl that = (XsltElementImpl)o;

        return myElement.equals(that.myElement);
    }

    @Override
    public PsiFile getContainingFile() {
        return myElement.getContainingFile();
    }

    @Override
    public boolean isWritable() {
        return myElement.isWritable();
    }

    @Override
    public PsiElement getParent() {
//        final XmlTag parent = PsiTreeUtil.getParentOfType(myElement.getParent(), XmlTag.class);
//        return parent != null ?
//                XsltSupport.isXsltTag(parent) ? myElementFactory.wrapElement(parent) : parent
//                : null;

        return myElement.getParent(); // TODO: return XSLT object
    }

    @Override
    public PsiElement @NotNull [] getChildren() {
        return myElement.getChildren(); // TODO: return XSLT objects
    }

    @Override
    public TextRange getTextRange() {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.getTextRange() : myElement.getTextRange();
    }

    @Override
    public int getStartOffsetInParent() {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.getStartOffsetInParent() : myElement.getStartOffsetInParent();
    }

    @Override
    public char @NotNull [] textToCharArray() {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.textToCharArray() : myElement.textToCharArray();
    }

    @Override
    public int getTextOffset() {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.getTextOffset() : myElement.getTextOffset();
    }

    @Override
    public ASTNode getNode() {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.getNode() : myElement.getNode();
    }

    @Override
    public PsiReference @NotNull [] getReferences() {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.getReferences() : myElement.getReferences();
    }

    @Override
    public boolean textMatches(@NotNull CharSequence charSequence) {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.textMatches(charSequence) : myElement.textMatches(charSequence);
    }

    @Override
    public boolean textMatches(@NotNull PsiElement psiElement) {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.textMatches(psiElement) : myElement.textMatches(psiElement);
    }

    @Override
    public PsiElement findElementAt(int i) {
        final XmlAttributeValue nameElement = getNameElement();
        return nameElement != null ? nameElement.findElementAt(i) : myElement.findElementAt(i);
    }

    @Override
    public void delete() throws IncorrectOperationException {
        myElement.delete();
    }

    protected static <S, T extends S> T[] convertArray(S[] elements, Class<T> aClass) {
        final T[] t = ArrayUtil.newArray(aClass, elements.length);
        //noinspection SuspiciousSystemArraycopy
        System.arraycopy(elements, 0, t, 0, elements.length);
        return t;
    }

  public ContextProvider getXPathContext() {
    throw new UnsupportedOperationException();
  }

  public XPathVersion getXPathVersion() {
    throw new UnsupportedOperationException();
  }
}
