package com.intellij.flex.uiDesigner;

import com.intellij.flex.uiDesigner.io.ByteArrayOutputStreamEx;
import com.intellij.flex.uiDesigner.io.PrimitiveAmfOutputStream;
import com.intellij.flex.uiDesigner.io.StringRegistry;
import com.intellij.flex.uiDesigner.mxml.MxmlUtil;
import com.intellij.flex.uiDesigner.mxml.PrimitiveWriter;
import com.intellij.flex.uiDesigner.mxml.XmlAttributeValueProvider;
import com.intellij.flex.uiDesigner.mxml.XmlElementValueProvider;
import com.intellij.injected.editor.VirtualFileWindow;
import com.intellij.javascript.flex.FlexAnnotationNames;
import com.intellij.javascript.flex.FlexPredefinedTagNames;
import com.intellij.javascript.flex.FlexReferenceContributor;
import com.intellij.javascript.flex.mxml.schema.ClassBackedElementDescriptor;
import com.intellij.lang.javascript.JavaScriptSupportLoader;
import com.intellij.lang.javascript.flex.AnnotationBackedDescriptor;
import com.intellij.lang.javascript.psi.JSCommonTypeNames;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.css.StylesheetFile;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.ui.update.Update;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import org.jetbrains.annotations.Nullable;

import static com.intellij.flex.uiDesigner.DocumentFactoryManager.DocumentInfo;

final class IncrementalDocumentSynchronizer extends Update {
  private final PsiTreeChangeEvent event;
  private boolean isSkippedXml;
  private boolean isStyleDataChanged;

  IncrementalDocumentSynchronizer(PsiTreeChangeEvent event) {
    super("FlashUIDesigner.incrementalUpdate");
    this.event = event;
  }

  @Override
  public boolean canEat(Update update) {
    if (!(update instanceof IncrementalDocumentSynchronizer)) {
      return false;
    }

    PsiTreeChangeEvent otherEvent = ((IncrementalDocumentSynchronizer)update).event;
    if (event.getFile() != otherEvent.getFile()) {
      return false;
    }

    // todo we don't support incremental update for CSS
    if (event.getFile() instanceof StylesheetFile) {
      return true;
    }

    return event.getParent() == otherEvent.getParent() &&
           event.getElement() == otherEvent.getElement();
  }

  @Override
  public void run() {
    DesignerApplicationManager designerManager = DesignerApplicationManager.getInstance();
    if (designerManager.isInitialRendering() || designerManager.isApplicationClosed()) {
      return;
    }

    // PsiTreeChangeEvent dispatched only for root psi file, i.e not for injected
    // (so, if CSS is injected, we get psi event about mxml file, but not about injected css file)
    // WELL, IT IS NOT TRUE!!! YESTERDAY IT ALWAYS WAS XmlFile, but TODAY IT IS CssFile :)
    final XmlFile xmlFile;
    if (event.getFile() instanceof XmlFile) {
      xmlFile = (XmlFile)event.getFile();
    }
    else {
      assert event.getFile() instanceof StylesheetFile;
      styleChanged();
      return;
    }

    DocumentInfo info = DocumentFactoryManager.getInstance().getNullableInfo(xmlFile);
    if (info != null && !incrementalSync(info)) {
      if (isStyleDataChanged) {
        styleChanged();
      }
      else if (!isSkippedXml) {
        initialRender(designerManager, xmlFile);
      }
    }
  }

  private void styleChanged() {
    // BE AWARE!!! INJECTION BEHAVIOR IS NOT PREDICTABLE, file may be injected.
    //noinspection ConstantConditions
    VirtualFile file = event.getFile().getViewProvider().getVirtualFile();
    if (file instanceof VirtualFileWindow) {
      file = ((VirtualFileWindow)file).getDelegate();
    }

    DesignerApplicationManager.getInstance().renderDocumentsAndCheckLocalStyleModification(
      new Document[]{FileDocumentManager.getInstance().getCachedDocument(file)}, true, false);
  }

  @Nullable
  private XmlElementValueProvider findSupportedTarget() {
    PsiElement element = event.getParent();
    // if we change attribute value via line marker, so, event.getParent() will be XmlAttribute instead of XmlAttributeValue
    while (!(element instanceof XmlAttribute)) {
      element = element.getParent();
      if (element instanceof XmlTag) {
        XmlTag tag = (XmlTag)element;
        XmlElementDescriptor descriptor = tag.getDescriptor();
        if (descriptor instanceof ClassBackedElementDescriptor) {
          ClassBackedElementDescriptor classBackedElementDescriptor = (ClassBackedElementDescriptor)descriptor;
          if (classBackedElementDescriptor.isPredefined()) {
            isStyleDataChanged = descriptor.getQualifiedName().equals(FlexPredefinedTagNames.STYLE);
            isSkippedXml = isStyleDataChanged ||
                           (!MxmlUtil.isObjectLanguageTag(tag) &&
                            !descriptor.getQualifiedName().equals(FlexPredefinedTagNames.DECLARATIONS));
          }
        }
        return null;
      }
      else if (element instanceof PsiFile || element == null) {
        return null;
      }
    }

    XmlAttribute attribute = (XmlAttribute)element;
    if (JavaScriptSupportLoader.MXML_URI3.equals(attribute.getNamespace()) || attribute.getValueElement() == null) {
      return null;
    }

    XmlAttributeDescriptor xmlDescriptor = attribute.getDescriptor();
    if (!(xmlDescriptor instanceof AnnotationBackedDescriptor)) {
      return null;
    }

    AnnotationBackedDescriptor descriptor = (AnnotationBackedDescriptor)xmlDescriptor;
    if (descriptor.isPredefined() || MxmlUtil.isIdLanguageAttribute(attribute, descriptor)) {
      return null;
    }

    // todo incremental sync for state-specific attributes
    PsiReference[] references = attribute.getReferences();
    if (references.length > 1) {
      for (int i = references.length - 1; i > -1; i--) {
        PsiReference psiReference = references[i];
        if (psiReference instanceof FlexReferenceContributor.StateReference) {
          return null;
        }
      }
    }
    else {
      String prefix = attribute.getName() + '.';
      for (XmlAttribute anotherAttribute : attribute.getParent().getAttributes()) {
        if (anotherAttribute != attribute && anotherAttribute.getName().startsWith(prefix)) {
          return null;
        }
      }
    }

    XmlAttributeValueProvider valueProvider = new XmlAttributeValueProvider(attribute);
    // skip binding
    PsiLanguageInjectionHost injectedHost = valueProvider.getInjectedHost();
    if (injectedHost != null && InjectedLanguageUtil.hasInjections(injectedHost)) {
      return null;
    }

    return valueProvider;
  }

  public static void initialRender(DesignerApplicationManager designerManager, XmlFile xmlFile) {
    designerManager.renderIfNeed(xmlFile, null);
  }

  private boolean incrementalSync(final DocumentInfo info) {
    final XmlElementValueProvider valueProvider = findSupportedTarget();
    if (valueProvider == null) {
      return false;
    }

    XmlTag tag = (XmlTag)valueProvider.getElement().getParent();
    if (!(tag.getDescriptor() instanceof ClassBackedElementDescriptor)) {
      return false;
    }

    int componentId = info.rangeMarkerIndexOf(tag);
    if (componentId == -1) {
      return false;
    }

    final AnnotationBackedDescriptor descriptor = (AnnotationBackedDescriptor)valueProvider.getPsiMetaData();
    assert descriptor != null;
    final String typeName = descriptor.getTypeName();
    final String type = descriptor.getType();
    if (type == null) {
      return !typeName.equals(FlexAnnotationNames.EFFECT);
    }
    else if (JSCommonTypeNames.FUNCTION_CLASS_NAMES.contains(type) || typeName.equals(FlexAnnotationNames.EVENT)) {
      return true;
    }

    final StringRegistry.StringWriter stringWriter = new StringRegistry.StringWriter();
    final PrimitiveAmfOutputStream dataOut = new PrimitiveAmfOutputStream(new ByteArrayOutputStreamEx(16));
    PrimitiveWriter writer = new PrimitiveWriter(dataOut, stringWriter);
    boolean needRollbackStringWriter = true;
    try {
      if (descriptor.isAllowsPercentage()) {
        String value = valueProvider.getTrimmed();
        final boolean hasPercent;
        if (value.isEmpty() || ((hasPercent = value.endsWith("%")) && value.length() == 1)) {
          return true;
        }

        final String name;
        if (hasPercent) {
          name = descriptor.getPercentProxy();
          value = value.substring(0, value.length() - 1);
        }
        else {
          name = descriptor.getName();
        }

        stringWriter.write(name, dataOut);
        dataOut.writeAmfDouble(value);
      }
      else {
        stringWriter.write(descriptor.getName(), dataOut);
        if (!writer.writeIfApplicable(valueProvider, dataOut, descriptor)) {
          needRollbackStringWriter = false;
          stringWriter.rollback();
          return false;
        }
      }

      needRollbackStringWriter = false;
    }
    catch (InvalidPropertyException ignored) {
      return true;
    }
    catch (NumberFormatException ignored) {
      return true;
    }
    finally {
      if (needRollbackStringWriter) {
        stringWriter.rollback();
      }
    }

    Client.getInstance().updatePropertyOrStyle(info.getId(), componentId, stream -> {
      stringWriter.writeTo(stream);
      stream.write(descriptor.isStyle());
      dataOut.writeTo(stream);
    }).doWhenDone(() -> DesignerApplicationManager.createDocumentRenderedNotificationDoneHandler(true).consume(info));

    return true;
  }

}