package com.intellij.flex.uiDesigner.abc;
import com.intellij.openapi.vfs.VirtualFile;
import gnu.trove.TIntArrayList;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.TestOnly;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static com.intellij.flex.uiDesigner.abc.PlaceObjectFlags.*;
public class MovieSymbolTranscoder extends SymbolTranscoderBase {
private byte[] symbolName;
private TIntObjectHashMap<PlacedObject> placedObjects;
// we cannot mark placedObject as used and iterate existing map (placedObjects) - order of items in map is not predictable,
// but we must write placed object in the same order as it was read
private List<PlacedObject> usedPlacedObjects;
// symbolName - utf8 bytes
@TestOnly
public void transcode(File in, File out, byte[] symbolName) throws IOException {
this.symbolName = symbolName;
//noinspection IOResourceOpenedButNotSafelyClosed
transcode(new FileInputStream(in), in.length(), out, false);
}
public void transcode(@NotNull VirtualFile in, @NotNull File out, @NotNull String symbolName) throws IOException {
this.symbolName = symbolName.getBytes();
transcode(in, out);
}
@Override
protected void transcode(boolean writeBounds) throws IOException {
fileLength = SYMBOL_CLASS_TAG_FULL_LENGTH + SwfUtil.getWrapLength();
final PlacedObject exportedSymbol = transcode();
buffer.position(exportedSymbol.start + 2);
if (writeBounds) {
writeMovieBounds();
}
final byte[] symbolOwnClassAbc = getSymbolOwnClassAbc(buffer.getShort());
fileLength += symbolOwnClassAbc.length;
SwfUtil.header(fileLength, out);
writeUsedPlacedObjects();
writePlacedObject(exportedSymbol);
out.write(symbolOwnClassAbc);
writeSymbolClass(exportedSymbol.newId);
SwfUtil.footer(out);
}
private PlacedObject transcode() throws IOException {
placedObjects = new TIntObjectHashMap<>();
int spriteId = -1;
int tagStart;
analyze: while ((tagStart = buffer.position()) < buffer.limit()) {
final int tagCodeAndLength = buffer.getShort();
final int type = tagCodeAndLength >> 6;
int length = tagCodeAndLength & 0x3F;
if (length == 63) {
length = buffer.getInt();
}
final int position = buffer.position();
switch (type) {
case TagTypes.End:
break analyze;
case TagTypes.DefineShape:
case TagTypes.DefineShape2:
case TagTypes.DefineShape3:
case TagTypes.DefineShape4:
case TagTypes.DefineSprite:
placedObjects.put(buffer.getShort(), new PlacedObject(position, length, type, tagStart));
break;
case TagTypes.ExportAssets:
case TagTypes.SymbolClass:
spriteId = processExportAssetsOrSymbolClass();
if (spriteId == -1) {
break;
}
else {
break analyze;
}
}
buffer.position(position + length);
}
if (spriteId == -1) {
throw new IOException("Can't find symbol");
}
usedPlacedObjects = new ArrayList<>(placedObjects.size());
bounds = null;
final PlacedObject exportedSymbol = placedObjects.get(spriteId);
exportedSymbol.used = true;
processDefineSprite(exportedSymbol);
exportedSymbol.newId = usedPlacedObjects.size() + 1;
fileLength += exportedSymbol.fileLength();
return exportedSymbol;
}
private void processDefineSprite(PlacedObject placedObject) throws IOException {
buffer.position(placedObject.start + 4);
final int endPosition = placedObject.start + placedObject.length;
while (true) {
final int tagStart = buffer.position();
final int tagCodeAndLength = buffer.getShort();
final int type = tagCodeAndLength >> 6;
int length = tagCodeAndLength & 0x3F;
if (length == 63) {
length = buffer.getInt();
}
final int start = buffer.position();
switch (type) {
case TagTypes.DoAction:
case TagTypes.DoInitAction:
placedObject.prepareSparseWrite();
if (placedObject.positions == null) {
placedObject.positions = new TIntArrayList();
placedObject.actualLength = placedObject.length;
}
placedObject.positions.add(tagStart);
final int fullLength = length + (start - tagStart);
placedObject.positions.add(tagStart + fullLength);
placedObject.actualLength -= fullLength;
continue;
case TagTypes.PlaceObject:
case TagTypes.PlaceObject3:
throw new IOException("PlaceObject and PlaceObject3 are not supported");
case TagTypes.PlaceObject2:
processPlaceObject2(placedObject, length, start);
break;
}
final int newPosition = start + length;
if (newPosition < endPosition) {
buffer.position(newPosition);
}
else {
break;
}
}
}
private static int computeFullLength(int length) {
return recordHeaderLength(length) + length;
}
private void processPlaceObject2(final PlacedObject placedObject, final int length, final int position) throws IOException {
int flags = buffer.get();
int objectIdPosition = -1;
if ((flags & HAS_CLIP_ACTION) != 0) {
flags &= ~HAS_CLIP_ACTION;
int bufferPosition = buffer.position();
buffer.put(bufferPosition - 1, (byte)flags);
bufferPosition += 2; // Depth
if ((flags & HAS_CHARACTER) != 0) {
objectIdPosition = bufferPosition;
bufferPosition += 2;
}
if ((flags & HAS_MATRIX) != 0) {
buffer.position(bufferPosition);
decodeMatrix();
bufferPosition = buffer.position();
}
if ((flags & HAS_COLOR_TRANSFORM) != 0) {
decodeColorTransform();
bufferPosition = buffer.position();
}
if ((flags & HAS_RATIO) != 0) {
bufferPosition += 2;
}
if ((flags & HAS_NAME) != 0) {
final int nameLengthWithTerminator = skipAbcName(bufferPosition) + 1;
bufferPosition += nameLengthWithTerminator;
}
if ((flags & HAS_CLIP_DEPTH) != 0) {
bufferPosition += 2;
}
placedObject.prepareSparseWrite();
placedObject.actualLength -= length - (bufferPosition - position);
placedObject.positions.add(bufferPosition);
placedObject.positions.add(position + length);
}
else if ((flags & HAS_CHARACTER) != 0) {
objectIdPosition = buffer.position() + 2;
}
// swf spec: "CharacterId is used only when a new character is being added. If a character that is already on the display map is being modified, the CharacterId field is absent."
// but in any case we check and use flag referredObject.used - swf may be invalid (but this problem is not encountered yet, develar 05.08.11)
if (objectIdPosition != -1) {
final int objectId = buffer.getShort(objectIdPosition);
final PlacedObject referredObject = placedObjects.get(objectId);
if (referredObject.used) {
return;
}
referredObject.used = true;
if (referredObject.tagType == TagTypes.DefineSprite) {
processDefineSprite(referredObject);
fileLength += referredObject.fileLength();
}
else if (bounds == null) {
findBounds(referredObject);
fileLength += referredObject.computeFullLengthAsProvided();
}
usedPlacedObjects.add(referredObject);
referredObject.newId = usedPlacedObjects.size();
buffer.putShort(objectIdPosition, (short)referredObject.newId);
}
}
private void findBounds(PlacedObject placedObject) throws IOException {
buffer.position(placedObject.start + 2);
decodeRect();
}
private void writeUsedPlacedObjects() throws IOException {
// must be written in the same order as it was read
Collections.sort(usedPlacedObjects, (o1, o2) -> o1.start < o2.start ? -1 : 1);
for (PlacedObject object : usedPlacedObjects) {
writePlacedObject(object);
}
}
private void writePlacedObject(PlacedObject object) throws IOException {
final TIntArrayList positions = object.positions;
if (positions == null) {
out.write(data, object.tagStart, object.start - object.tagStart);
// little-endian short new id
out.write(0xff & object.newId);
out.write(0xff & (object.newId >> 8));
out.write(data, object.start + 2, object.length - 2);
}
else {
buffer.position(0);
encodeTagHeader(object.tagType, object.actualLength);
buffer.putShort((short)object.newId);
out.write(data, 0, buffer.position());
writeSparseBytes(positions, object.start + 2, object.start + object.length);
}
}
private void decodeColorTransform() throws IOException {
syncBits();
boolean hasAdd = readBit();
@SuppressWarnings("SpellCheckingInspection")
boolean hasMult = readBit();
int nBits = readUBits(4);
if (hasMult) {
readSBits(nBits);
readSBits(nBits);
readSBits(nBits);
readSBits(nBits);
}
if (hasAdd) {
readSBits(nBits);
readSBits(nBits);
readSBits(nBits);
readSBits(nBits);
}
}
@SuppressWarnings("UnusedDeclaration")
private void decodeMatrix() throws IOException {
syncBits();
boolean hasScale = readBit();
if (hasScale) {
int nScaleBits = readUBits(5);
int scaleX = readSBits(nScaleBits);
int scaleY = readSBits(nScaleBits);
}
boolean hasRotate = readBit();
if (hasRotate) {
int nRotateBits = readUBits(5);
int rotateSkew0 = readSBits(nRotateBits);
int rotateSkew1 = readSBits(nRotateBits);
}
int nTranslateBits = readUBits(5);
int translateX = readSBits(nTranslateBits);
int translateY = readSBits(nTranslateBits);
}
private boolean readBit() throws IOException {
return readUBits(1) != 0;
}
private int processExportAssetsOrSymbolClass() {
final int numSymbols = buffer.getShort();
if (numSymbols == 0) {
return -1;
}
final byte[] data = buffer.array();
for (int i = 0; i < numSymbols; i++) {
final int id = buffer.getShort();
int j = buffer.position();
int k = 0;
while (true) {
final int b = data[j++];
if (b == 0) {
if (k == symbolName.length) {
return id;
}
else {
break;
}
}
if (b != symbolName[k++]) {
//noinspection StatementWithEmptyBody
while (data[j++] != 0) {
}
break;
}
}
buffer.position(j);
}
return -1;
}
private static class PlacedObject {
private boolean used;
private int newId = -1;
// we cannot calculate tagStart by length and start - length may be less than 63, but encoded as long tag header
private final int tagStart;
private final int start;
private final int length;
private final int tagType;
private int actualLength = -1;
private TIntArrayList positions;
public void prepareSparseWrite() {
if (positions == null) {
positions = new TIntArrayList();
actualLength = length;
}
}
private PlacedObject(int start, int length, int tagType, int tagStart) {
this.start = start;
this.length = length;
this.tagType = tagType;
this.tagStart = tagStart;
}
public int computeFullLengthAsProvided() {
return length + (start - tagStart);
}
public int fileLength() {
if (positions == null) {
// we encode length as provided, just copy bytes
return computeFullLengthAsProvided();
}
else {
// we encode length according to rules about long or short tag header
return computeFullLength(actualLength);
}
}
}
}