// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.util.xml.impl;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiAnchor;
import com.intellij.psi.PsiElement;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlElement;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.xml.*;
import com.intellij.util.xml.reflect.AbstractDomChildrenDescription;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;

public abstract class DomAnchorImpl<T extends DomElement> implements DomAnchor<T> {
  private static final Logger LOG = Logger.getInstance(DomAnchorImpl.class);

  public static <T extends DomElement> DomAnchor<T> createAnchor(@NotNull T t) {
    return createAnchor(t, true);
  }

  public static <T extends DomElement> DomAnchor<T> createAnchor(@NotNull T t, boolean usePsi) {
    DomInvocationHandler handler = DomManagerImpl.getNotNullHandler(t);
    if (handler.getStub() != null) {
      return new StubAnchor<>(handler);
    }

    if (usePsi) {
      final XmlElement element = t.getXmlElement();
      if (element != null) {
        return new PsiBasedDomAnchor<>(PsiAnchor.create(element), element.getProject());
      }
    }


    final DomElement parent = t.getParent();
    if (parent == null) {
      LOG.error("Parent null: " + t);
    }

    if (parent instanceof DomFileElementImpl fileElement) {
      //noinspection unchecked
      return new RootAnchor<>(fileElement.getFile(), fileElement.getRootElementClass());
    }

    final DomAnchor<DomElement> parentAnchor = createAnchor(parent);
    final String name = t.getGenericInfo().getElementName(t);
    final AbstractDomChildrenDescription description = t.getChildDescription();
    final List<? extends DomElement> values = description.getValues(parent);
    if (name != null) {
      int i = 0;
      for (DomElement value : values) {
        if (value.equals(t)) {
          return new NamedAnchor<>(parentAnchor, description, name, i);
        }
        if (name.equals(value.getGenericInfo().getElementName(value))) {
          i++;
        }
      }
    }

    final int index = values.indexOf(t);
    if (index < 0) {
      diagnoseNegativeIndex2(t, parent, description, values);
    }
    return new IndexedAnchor<>(parentAnchor, description, index);
  }

  private static <T extends DomElement> void diagnoseNegativeIndex2(T t,
                                                                    DomElement parent,
                                                                    AbstractDomChildrenDescription description,
                                                                    List<? extends DomElement> values) {
    final XmlTag parentTag = parent.getXmlTag();
    StringBuilder diag = new StringBuilder("Index<0: description=" + description + "\nparent=" + parent + "\nt=" + t + "\nvalues=" + values + "\n");
    for (int i = 0, size = values.size(); i < size; i++) {
      DomElement value = values.get(i);
      if (value.toString().equals(t.toString())) {
        final XmlElement tElement = t.getXmlElement();
        final XmlElement valElement = value.getXmlElement();
        diag.append(" hasSame, i=" + i +
                    "; same=" + (value == t) +
                    ", equal=" + value.equals(t) +
                    ", equal2=" + t.equals(value) +
                    ", t.physical=" + (tElement == null ? "null" : String.valueOf(tElement.isPhysical())) +
                    ", value.physical=" + (valElement == null ? "null" : String.valueOf(valElement.isPhysical())) +
                    ", sameElements=" + (tElement == value.getXmlElement()) +
                    "\n");
        if (tElement != null && valElement != null) {
          diag.append("  sameFile=" + (tElement.getContainingFile() == valElement.getContainingFile()) +
                      ", sameParent=" + (tElement.getParent() == valElement.getParent()) +
                      "\n");
        }
      }
    }

    if (parentTag != null) {
      diag.append("Parent tag: ").append(parentTag.getName()).append("\n");
      if (t instanceof GenericAttributeValue) {
        for (XmlAttribute attribute : parentTag.getAttributes()) {
          diag.append(", attr: ").append(attribute.getName());
        }
      } else {
        for (XmlTag tag : parentTag.getSubTags()) {
          diag.append("\n subtag: ").append(tag.getName());
        }
      }
      diag.append("\n");
    }
    diag.append("Child name: ").append(t.getXmlElementName()).append(";").append(t.getXmlElementNamespaceKey());
    LOG.error(diag.toString());
  }


  @Override
  public PsiElement getPsiElement() {
    T t = retrieveDomElement();
    return t == null ? null : t.getXmlElement();
  }

  @Override
  public abstract @Nullable T retrieveDomElement();

  @Override
  public abstract @NotNull XmlFile getContainingFile();

  private static final class NamedAnchor<T extends DomElement> extends DomAnchorImpl<T> {
    private final DomAnchor myParent;
    private final AbstractDomChildrenDescription myDescr;
    private final String myName;
    private final int myIndex;

    private NamedAnchor(final DomAnchor parent, final AbstractDomChildrenDescription descr, final String id, int index) {
      myParent = parent;
      myDescr = descr;
      myName = id;
      myIndex = index;
    }

    @Override
    public boolean equals(final Object o) {
      if (this == o) return true;
      if (!(o instanceof NamedAnchor that)) return false;

      if (myDescr != null ? !myDescr.equals(that.myDescr) : that.myDescr != null) return false;
      if (myName != null ? !myName.equals(that.myName) : that.myName != null) return false;
      if (myParent != null ? !myParent.equals(that.myParent) : that.myParent != null) return false;
      if (myIndex != that.myIndex) return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result;
      result = (myParent != null ? myParent.hashCode() : 0);
      result = 31 * result + (myDescr != null ? myDescr.hashCode() : 0);
      result = 31 * result + (myName != null ? myName.hashCode() : 0);
      result = 31 * result + myIndex;
      return result;
    }

    @Override
    public T retrieveDomElement() {
      final DomElement parent = myParent.retrieveDomElement();
      if (parent == null) return null;

      final List<? extends DomElement> list = myDescr.getValues(parent);
      int i = 0;
      for (final DomElement element : list) {
        final String s = element.getGenericInfo().getElementName(element);
        if (myName.equals(s)) {
          if (i == myIndex) {
            //noinspection unchecked
            return (T)element;
          }
          i++;
        }
      }
      return null;
    }

    @Override
    public @NotNull XmlFile getContainingFile() {
      return myParent.getContainingFile();
    }
  }

  private static final class IndexedAnchor<T extends DomElement> extends DomAnchorImpl<T> {
    private final DomAnchor myParent;
    private final AbstractDomChildrenDescription myDescr;
    private final int myIndex;

    private IndexedAnchor(final DomAnchor parent, final AbstractDomChildrenDescription descr, final int index) {
      myParent = parent;
      myDescr = descr;
      myIndex = index;
    }

    @Override
    public boolean equals(final Object o) {
      if (this == o) return true;
      if (!(o instanceof IndexedAnchor that)) return false;

      if (myIndex != that.myIndex) return false;
      if (myDescr != null ? !myDescr.equals(that.myDescr) : that.myDescr != null) return false;
      if (myParent != null ? !myParent.equals(that.myParent) : that.myParent != null) return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result;
      result = (myParent != null ? myParent.hashCode() : 0);
      result = 31 * result + (myDescr != null ? myDescr.hashCode() : 0);
      result = 31 * result + myIndex;
      return result;
    }

    @Override
    public T retrieveDomElement() {
      final DomElement parent = myParent.retrieveDomElement();
      if (parent == null) return null;

      final List<? extends DomElement> list = myDescr.getValues(parent);
      if (myIndex < 0 || myIndex >= list.size()) return null;

      //noinspection unchecked
      return (T)list.get(myIndex);
    }

    @Override
    public @NotNull XmlFile getContainingFile() {
      return myParent.getContainingFile();
    }
  }

  private static final class RootAnchor<T extends DomElement> extends DomAnchorImpl<T> {
    private final XmlFile myFile;
    private final Class<T> myClass;

    private RootAnchor(final XmlFile file, final Class<T> aClass) {
      myFile = file;
      myClass = aClass;
    }

    @Override
    public boolean equals(final Object o) {
      if (this == o) return true;
      if (!(o instanceof RootAnchor that)) return false;

      if (myClass != null ? !myClass.equals(that.myClass) : that.myClass != null) return false;
      if (myFile != null ? !myFile.equals(that.myFile) : that.myFile != null) return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result;
      result = (myFile != null ? myFile.hashCode() : 0);
      result = 31 * result + (myClass != null ? myClass.hashCode() : 0);
      return result;
    }

    @Override
    public T retrieveDomElement() {
      final DomFileElement<T> fileElement = DomManager.getDomManager(myFile.getProject()).getFileElement(myFile, myClass);
      return fileElement == null ? null : fileElement.getRootElement();
    }

    @Override
    public @NotNull XmlFile getContainingFile() {
      return myFile;
    }
  }


  private static class PsiBasedDomAnchor<T extends DomElement> extends DomAnchorImpl<T> {
    private final PsiAnchor myAnchor;
    private final Project myProject;

    PsiBasedDomAnchor(PsiAnchor anchor, Project project) {
      myAnchor = anchor;
      myProject = project;
    }

    @Override
    public T retrieveDomElement() {
      PsiElement psi = myAnchor.retrieve();
      if (psi == null) return null;

      if (psi instanceof XmlTag) {
        return (T)DomManager.getDomManager(myProject).getDomElement((XmlTag)psi);
      }
      if (psi instanceof XmlAttribute) {
        return (T)DomManager.getDomManager(myProject).getDomElement((XmlAttribute)psi);
      }
      return null;
    }

    @Override
    public @NotNull XmlFile getContainingFile() {
      return (XmlFile)myAnchor.getFile();
    }

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

      PsiBasedDomAnchor anchor = (PsiBasedDomAnchor)o;

      if (myAnchor != null ? !myAnchor.equals(anchor.myAnchor) : anchor.myAnchor != null) return false;
      if (myProject != null ? !myProject.equals(anchor.myProject) : anchor.myProject != null) return false;

      return true;
    }


    @Override
    public int hashCode() {
      int result = myAnchor != null ? myAnchor.hashCode() : 0;
      result = 31 * result + (myProject != null ? myProject.hashCode() : 0);
      return result;
    }
  }

  private static final class StubAnchor<T extends DomElement> implements DomAnchor<T> {

    private final DomInvocationHandler myHandler;

    private StubAnchor(DomInvocationHandler handler) {
      myHandler = handler;
    }

    @Override
    public @NotNull T retrieveDomElement() {
      return (T)myHandler.getProxy();
    }

    @Override
    public @NotNull XmlFile getContainingFile() {
      return myHandler.getFile();
    }

    @Override
    public @Nullable PsiElement getPsiElement() {
      return myHandler.getXmlElement();
    }

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

      StubAnchor anchor = (StubAnchor)o;

      if (myHandler != null ? !myHandler.equals(anchor.myHandler) : anchor.myHandler != null) return false;

      return true;
    }

    @Override
    public int hashCode() {
      return myHandler != null ? myHandler.hashCode() : 0;
    }
  }
}
