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;
public 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 (type.equals(JSCommonTypeNames.FUNCTION_CLASS_NAME) || typeName.equals(FlexAnnotationNames.EVENT)) {
return true;
}
final StringRegistry.StringWriter stringWriter = new StringRegistry.StringWriter();
//noinspection IOResourceOpenedButNotSafelyClosed
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;
}
}