/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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 com.intellij.util.xml;

import com.intellij.codeInsight.navigation.actions.GotoDeclarationAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.PomTargetPsiElement;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.xml.XmlFile;
import com.intellij.testFramework.Timings;
import com.intellij.testFramework.fixtures.LightJavaCodeInsightFixtureTestCase;
import com.intellij.util.Consumer;
import com.intellij.util.xml.impl.DomFileElementImpl;
import com.intellij.util.xml.impl.DomManagerImpl;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

public class DomIncludesTest extends LightJavaCodeInsightFixtureTestCase {

  public void testGetChildrenHonorsIncludes() throws Throwable {
    final MyElement rootElement = createDomFile("a.xml", "<root xmlns:xi=\"http://www.w3.org/2001/XInclude\">" +
                            "<xi:include href=\"b.xml\" xpointer=\"xpointer(/xxx/*)\"/>" +
                            "<child xxx=\"a\"/>" +
                            "</root>");
    createFile("b.xml", "<xxx><child xxx=\"b\"/></xxx>");
    final List<DomIncludesTest.Child> children = rootElement.getChildren();
    Consumer<MyElement> consumer1 = element -> {
      assertFalse(element.getXmlElement().isPhysical());
      assertEquals(rootElement, element.getParent());
      assertEquals(DomUtil.getFileElement(rootElement), DomUtil.getFileElement(element));
      assertEquals("b", element.getXxx().getValue());
    };
    Consumer<MyElement> consumer2 = element -> {
      assertTrue(element.getXmlElement().isPhysical());
      assertEquals(rootElement, element.getParent());
      assertEquals(DomUtil.getFileElement(rootElement), DomUtil.getFileElement(element));
      assertEquals("a", element.getXxx().getValue());
    };
    assertOrderedCollection(children, consumer1, consumer2);
  }

  public void testNamespaces() throws Throwable {
    final MyElement rootElement = createDomFile("a.xml", "<root xmlns:xi=\"http://www.w3.org/2001/XInclude\" xmlns:foo=\"foo\">" +
                            "<xi:include href=\"b.xml\" xpointer=\"xpointer(/xxx/*)\"/>" +
                            "<foo:boy xxx=\"a\"/>" +
                            "</root>");
    DomUtil.getFileElement(rootElement).getFileDescription().registerNamespacePolicy("foo", "foo");

    createFile("b.xml", "<xxx xmlns:foo=\"foo\"><foo:boy xxx=\"b\"/></xxx>");
    final List<Boy> children = rootElement.getBoys();
    assertOrderedCollection(children, element -> {
      assertFalse(element.getXmlElement().isPhysical());
      assertEquals(rootElement, element.getParent());
      assertEquals(DomUtil.getFileElement(rootElement), DomUtil.getFileElement(element));
      assertEquals("b", element.getXxx().getValue());
    }, element -> {
      assertTrue(element.getXmlElement().isPhysical());
      assertEquals(rootElement, element.getParent());
      assertEquals(DomUtil.getFileElement(rootElement), DomUtil.getFileElement(element));
      assertEquals("a", element.getXxx().getValue());
    });
  }

  public void testEqualsWithMultiThreadedIncludes() throws Throwable {
    final MyElement rootElement = createDomFile("a.xml", "<root xmlns:xi=\"http://www.w3.org/2001/XInclude\">" +
                            "<xi:include href=\"b.xml\" xpointer=\"xpointer(/xxx/*)\"/>" +
                            "</root>");

    final String textB =
      "<xxx><boy/><xi:include href=\"c.xml\" xpointer=\"xpointer(/xxx/*)\"/><child/><xi:include href=\"c.xml\" xpointer=\"xpointer(/xxx/*)\"/></xxx>";
    final PsiFile fileB = createFile("b.xml", textB);
    final String textC =
      "<xxx><child/><xi:include href=\"d.xml\" xpointer=\"xpointer(/xxx/*)\"/><boy/><xi:include href=\"d.xml\" xpointer=\"xpointer(/xxx/*)\"/></xxx>";
    final PsiFile fileC = createFile("c.xml", textC);
    final String textD =
      "<xxx><boy/><xi:include href=\"e.xml\" xpointer=\"xpointer(/xxx/*)\"/><child/><xi:include href=\"e.xml\" xpointer=\"xpointer(/xxx/*)\"/></xxx>";
    final PsiFile fileD = createFile("d.xml", textD);
    final String textE = "<xxx><boy/><child/><boy/><child/><boy/><child/><boy/><child/><boy/><child/><boy/><child/></xxx>";
    final PsiFile fileE = createFile("e.xml", textE);
    final int threadCount = 20;
    final int iterationCount = Timings.adjustAccordingToMySpeed(100, true);
    LOG.debug("iterationCount = " + iterationCount);

    final CountDownLatch finished = new CountDownLatch(threadCount);
    final AtomicReference<Exception> ex = new AtomicReference<>();

    for (int j = 0; j < threadCount; j++) {
      ApplicationManager.getApplication().executeOnPooledThread(() -> {
        try {
          for (int k = 0; k < iterationCount; k++) {
            ApplicationManager.getApplication().runReadAction(() -> {
              final List<Boy> boys = rootElement.getBoys();
              Thread.yield();
              final List<Child> children = rootElement.getChildren();
              Thread.yield();
              assertEquals(boys, rootElement.getBoys());
              assertEquals(children, rootElement.getChildren());
            });
            Thread.yield();
          }
        }
        catch (Exception e) {
          ex.set(e);
        }
        finally {
          finished.countDown();
        }
      });
    }

    for (int i = 0; i < iterationCount; i++) {
      WriteCommandAction.runWriteCommandAction(null, () -> {
        fileB.getViewProvider().getDocument().insertString(0, " ");
        fileD.getViewProvider().getDocument().insertString(0, " ");
        PsiDocumentManager.getInstance(getProject()).commitAllDocuments(); //clear xinclude caches
      });
      Thread.sleep(10);
      WriteCommandAction.runWriteCommandAction(null, () -> {
        fileC.getViewProvider().getDocument().insertString(0, " ");
        fileE.getViewProvider().getDocument().insertString(0, " ");
        PsiDocumentManager.getInstance(getProject()).commitAllDocuments(); //clear xinclude caches
      });
      Thread.sleep(10);
      WriteCommandAction.runWriteCommandAction(null, () -> {

        fileB.getViewProvider().getDocument().setText(textB);
        fileC.getViewProvider().getDocument().setText(textC);
        fileD.getViewProvider().getDocument().setText(textD);
        fileE.getViewProvider().getDocument().setText(textE);
        PsiDocumentManager.getInstance(getProject()).commitAllDocuments(); //clear xinclude caches
      });
    }

    finished.await();
    final Exception exception = ex.get();
    if (exception != null) {
      throw exception;
    }
  }

  public void testNavigationToIncluded() throws Throwable {
    final MyElement rootElement = createDomFile("a.xml", "<root xmlns:xi=\"http://www.w3.org/2001/XInclude\">" +
                            "<xi:include href=\"b.xml\" xpointer=\"xpointer(/xxx/*)\"/>" +
                            "<child ref=\"b\"/>" +
                            "</root>");
    final XmlFile includedFile = (XmlFile) createFile("b.xml", "<xxx><child xxx=\"b\"/></xxx>");
    final List<Child> children = rootElement.getChildren();
    final MyElement domTarget = children.get(0);
    final GenericAttributeValue<Child> ref = children.get(1).getRef();
    final MyElement value = ref.getValue();
    assertEquals(domTarget, value);

    myFixture.configureFromTempProjectFile("a.xml");

    final int offset = ref.getXmlAttributeValue().getTextRange().getStartOffset() + 1;
    myFixture.getEditor().getCaretModel().moveToOffset(offset);
    final PsiElement target = GotoDeclarationAction.findTargetElement(getProject(), myFixture.getEditor(), offset);
    PsiElement element = ((DomTarget)((PomTargetPsiElement)target).getTarget()).getNavigationElement();
//    assertSame(PomService.convertToPsi(DomTarget.getTarget(domTarget)), target);
    assertSame(includedFile.getDocument().getRootTag().getSubTags()[0].getAttributes()[0].getValueElement(), element);
  }

  private MyElement createDomFile(final String fileName, final String fileText) throws IOException {
    final XmlFile xmlFile = (XmlFile) createFile(fileName, fileText);
    final DomFileElementImpl<MyElement> element = getDomManager().getFileElement(xmlFile, MyElement.class, xmlFile.getDocument().getRootTag().getName());
    return element.getRootElement();
  }

  private PsiFile createFile(final String fileName, final String fileText) throws IOException {
    final VirtualFile file = myFixture.getTempDirFixture().createFile(fileName, fileText);
    PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
    return myFixture.getPsiManager().findFile(file);
  }

  private DomManagerImpl getDomManager() {
    return DomManagerImpl.getDomManager(getProject());
  }

  public interface MyElement extends DomElement {

    List<Child> getChildren();

    List<Boy> getBoys();

    @NameValue
    GenericAttributeValue<String> getXxx();

    GenericAttributeValue<Child> getRef();

  }

  @Namespace("foo")
  public interface Boy extends MyElement {

  }

  public interface Child extends MyElement {

  }

}
