/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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.google.android.exoplayer2.text.cea;

import android.graphics.Color;
import android.graphics.Typeface;
import android.text.Layout.Alignment;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.Cue.AnchorType;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoder;
import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.ParsableBitArray;
import com.google.android.exoplayer2.util.ParsableByteArray;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * A {@link SubtitleDecoder} for CEA-708 (also known as "EIA-708").
 */
public final class Cea708Decoder extends CeaDecoder {

  private static final String TAG = "Cea708Decoder";

  private static final int NUM_WINDOWS = 8;

  private static final int DTVCC_PACKET_DATA = 0x02;
  private static final int DTVCC_PACKET_START = 0x03;
  private static final int CC_VALID_FLAG = 0x04;

  // Base Commands
  private static final int GROUP_C0_END = 0x1F;  // Miscellaneous Control Codes
  private static final int GROUP_G0_END = 0x7F;  // ASCII Printable Characters
  private static final int GROUP_C1_END = 0x9F;  // Captioning Command Control Codes
  private static final int GROUP_G1_END = 0xFF;  // ISO 8859-1 LATIN-1 Character Set

  // Extended Commands
  private static final int GROUP_C2_END = 0x1F;  // Extended Control Code Set 1
  private static final int GROUP_G2_END = 0x7F;  // Extended Miscellaneous Characters
  private static final int GROUP_C3_END = 0x9F;  // Extended Control Code Set 2
  private static final int GROUP_G3_END = 0xFF;  // Future Expansion

  // Group C0 Commands
  private static final int COMMAND_NUL = 0x00;        // Nul
  private static final int COMMAND_ETX = 0x03;        // EndOfText
  private static final int COMMAND_BS = 0x08;         // Backspace
  private static final int COMMAND_FF = 0x0C;         // FormFeed (Flush)
  private static final int COMMAND_CR = 0x0D;         // CarriageReturn
  private static final int COMMAND_HCR = 0x0E;        // ClearLine
  private static final int COMMAND_EXT1 = 0x10;       // Extended Control Code Flag
  private static final int COMMAND_EXT1_START = 0x11;
  private static final int COMMAND_EXT1_END = 0x17;
  private static final int COMMAND_P16_START = 0x18;
  private static final int COMMAND_P16_END = 0x1F;

  // Group C1 Commands
  private static final int COMMAND_CW0 = 0x80;  // SetCurrentWindow to 0
  private static final int COMMAND_CW1 = 0x81;  // SetCurrentWindow to 1
  private static final int COMMAND_CW2 = 0x82;  // SetCurrentWindow to 2
  private static final int COMMAND_CW3 = 0x83;  // SetCurrentWindow to 3
  private static final int COMMAND_CW4 = 0x84;  // SetCurrentWindow to 4
  private static final int COMMAND_CW5 = 0x85;  // SetCurrentWindow to 5
  private static final int COMMAND_CW6 = 0x86;  // SetCurrentWindow to 6
  private static final int COMMAND_CW7 = 0x87;  // SetCurrentWindow to 7
  private static final int COMMAND_CLW = 0x88;  // ClearWindows (+1 byte)
  private static final int COMMAND_DSW = 0x89;  // DisplayWindows (+1 byte)
  private static final int COMMAND_HDW = 0x8A;  // HideWindows (+1 byte)
  private static final int COMMAND_TGW = 0x8B;  // ToggleWindows (+1 byte)
  private static final int COMMAND_DLW = 0x8C;  // DeleteWindows (+1 byte)
  private static final int COMMAND_DLY = 0x8D;  // Delay (+1 byte)
  private static final int COMMAND_DLC = 0x8E;  // DelayCancel
  private static final int COMMAND_RST = 0x8F;  // Reset
  private static final int COMMAND_SPA = 0x90;  // SetPenAttributes (+2 bytes)
  private static final int COMMAND_SPC = 0x91;  // SetPenColor (+3 bytes)
  private static final int COMMAND_SPL = 0x92;  // SetPenLocation (+2 bytes)
  private static final int COMMAND_SWA = 0x97;  // SetWindowAttributes (+4 bytes)
  private static final int COMMAND_DF0 = 0x98;  // DefineWindow 0 (+6 bytes)
  private static final int COMMAND_DF1 = 0x99;  // DefineWindow 1 (+6 bytes)
  private static final int COMMAND_DF2 = 0x9A;  // DefineWindow 2 (+6 bytes)
  private static final int COMMAND_DF3 = 0x9B;  // DefineWindow 3 (+6 bytes)
  private static final int COMMAND_DS4 = 0x9C;  // DefineWindow 4 (+6 bytes)
  private static final int COMMAND_DF5 = 0x9D;  // DefineWindow 5 (+6 bytes)
  private static final int COMMAND_DF6 = 0x9E;  // DefineWindow 6 (+6 bytes)
  private static final int COMMAND_DF7 = 0x9F;  // DefineWindow 7 (+6 bytes)

  // G0 Table Special Chars
  private static final int CHARACTER_MN = 0x7F;  // MusicNote

  // G2 Table Special Chars
  private static final int CHARACTER_TSP = 0x20;
  private static final int CHARACTER_NBTSP = 0x21;
  private static final int CHARACTER_ELLIPSIS = 0x25;
  private static final int CHARACTER_BIG_CARONS = 0x2A;
  private static final int CHARACTER_BIG_OE = 0x2C;
  private static final int CHARACTER_SOLID_BLOCK = 0x30;
  private static final int CHARACTER_OPEN_SINGLE_QUOTE = 0x31;
  private static final int CHARACTER_CLOSE_SINGLE_QUOTE = 0x32;
  private static final int CHARACTER_OPEN_DOUBLE_QUOTE = 0x33;
  private static final int CHARACTER_CLOSE_DOUBLE_QUOTE = 0x34;
  private static final int CHARACTER_BOLD_BULLET = 0x35;
  private static final int CHARACTER_TM = 0x39;
  private static final int CHARACTER_SMALL_CARONS = 0x3A;
  private static final int CHARACTER_SMALL_OE = 0x3C;
  private static final int CHARACTER_SM = 0x3D;
  private static final int CHARACTER_DIAERESIS_Y = 0x3F;
  private static final int CHARACTER_ONE_EIGHTH = 0x76;
  private static final int CHARACTER_THREE_EIGHTHS = 0x77;
  private static final int CHARACTER_FIVE_EIGHTHS = 0x78;
  private static final int CHARACTER_SEVEN_EIGHTHS = 0x79;
  private static final int CHARACTER_VERTICAL_BORDER = 0x7A;
  private static final int CHARACTER_UPPER_RIGHT_BORDER = 0x7B;
  private static final int CHARACTER_LOWER_LEFT_BORDER = 0x7C;
  private static final int CHARACTER_HORIZONTAL_BORDER = 0x7D;
  private static final int CHARACTER_LOWER_RIGHT_BORDER = 0x7E;
  private static final int CHARACTER_UPPER_LEFT_BORDER = 0x7F;

  private final ParsableByteArray ccData;
  private final ParsableBitArray serviceBlockPacket;

  private final int selectedServiceNumber;
  private final CueBuilder[] cueBuilders;

  private CueBuilder currentCueBuilder;
  private List<Cue> cues;
  private List<Cue> lastCues;

  private DtvCcPacket currentDtvCcPacket;
  private int currentWindow;

  public Cea708Decoder(int accessibilityChannel) {
    ccData = new ParsableByteArray();
    serviceBlockPacket = new ParsableBitArray();
    selectedServiceNumber = (accessibilityChannel == Format.NO_VALUE) ? 1 : accessibilityChannel;

    cueBuilders = new CueBuilder[NUM_WINDOWS];
    for (int i = 0; i < NUM_WINDOWS; i++) {
      cueBuilders[i] = new CueBuilder();
    }

    currentCueBuilder = cueBuilders[0];
    resetCueBuilders();
  }

  @Override
  public String getName() {
    return "Cea708Decoder";
  }

  @Override
  public void flush() {
    super.flush();
    cues = null;
    lastCues = null;
    currentWindow = 0;
    currentCueBuilder = cueBuilders[currentWindow];
    resetCueBuilders();
    currentDtvCcPacket = null;
  }

  @Override
  protected boolean isNewSubtitleDataAvailable() {
    return cues != lastCues;
  }

  @Override
  protected Subtitle createSubtitle() {
    lastCues = cues;
    return new CeaSubtitle(cues);
  }

  @Override
  protected void decode(SubtitleInputBuffer inputBuffer) {
    ccData.reset(inputBuffer.data.array(), inputBuffer.data.limit());
    while (ccData.bytesLeft() >= 3) {
      int ccTypeAndValid = (ccData.readUnsignedByte() & 0x07);

      int ccType = ccTypeAndValid & (DTVCC_PACKET_DATA | DTVCC_PACKET_START);
      boolean ccValid = (ccTypeAndValid & CC_VALID_FLAG) == CC_VALID_FLAG;
      byte ccData1 = (byte) ccData.readUnsignedByte();
      byte ccData2 = (byte) ccData.readUnsignedByte();

      // Ignore any non-CEA-708 data
      if (ccType != DTVCC_PACKET_DATA && ccType != DTVCC_PACKET_START) {
        continue;
      }

      if (!ccValid) {
        // This byte-pair isn't valid, ignore it and continue.
        continue;
      }

      if (ccType == DTVCC_PACKET_START) {
        finalizeCurrentPacket();

        int sequenceNumber = (ccData1 & 0xC0) >> 6; // first 2 bits
        int packetSize = ccData1 & 0x3F; // last 6 bits
        if (packetSize == 0) {
          packetSize = 64;
        }

        currentDtvCcPacket = new DtvCcPacket(sequenceNumber, packetSize);
        currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
      } else {
        // The only remaining valid packet type is DTVCC_PACKET_DATA
        Assertions.checkArgument(ccType == DTVCC_PACKET_DATA);

        if (currentDtvCcPacket == null) {
          Log.e(TAG, "Encountered DTVCC_PACKET_DATA before DTVCC_PACKET_START");
          continue;
        }

        currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData1;
        currentDtvCcPacket.packetData[currentDtvCcPacket.currentIndex++] = ccData2;
      }

      if (currentDtvCcPacket.currentIndex == (currentDtvCcPacket.packetSize * 2 - 1)) {
        finalizeCurrentPacket();
      }
    }
  }

  private void finalizeCurrentPacket() {
    if (currentDtvCcPacket == null) {
      // No packet to finalize;
      return;
    }

    processCurrentPacket();
    currentDtvCcPacket = null;
  }

  private void processCurrentPacket() {
    if (currentDtvCcPacket.currentIndex != (currentDtvCcPacket.packetSize * 2 - 1)) {
      Log.w(TAG, "DtvCcPacket ended prematurely; size is " + (currentDtvCcPacket.packetSize * 2 - 1)
          + ", but current index is " + currentDtvCcPacket.currentIndex + " (sequence number "
          + currentDtvCcPacket.sequenceNumber + "); ignoring packet");
      return;
    }

    serviceBlockPacket.reset(currentDtvCcPacket.packetData, currentDtvCcPacket.currentIndex);

    int serviceNumber = serviceBlockPacket.readBits(3);
    int blockSize = serviceBlockPacket.readBits(5);
    if (serviceNumber == 7) {
      // extended service numbers
      serviceBlockPacket.skipBits(2);
      serviceNumber += serviceBlockPacket.readBits(6);
    }

    // Ignore packets in which blockSize is 0
    if (blockSize == 0) {
      if (serviceNumber != 0) {
        Log.w(TAG, "serviceNumber is non-zero (" + serviceNumber + ") when blockSize is 0");
      }
      return;
    }

    if (serviceNumber != selectedServiceNumber) {
      return;
    }

    while (serviceBlockPacket.bitsLeft() > 0) {
      int command = serviceBlockPacket.readBits(8);
      if (command != COMMAND_EXT1) {
        if (command <= GROUP_C0_END) {
          handleC0Command(command);
        } else if (command <= GROUP_G0_END) {
          handleG0Character(command);
        } else if (command <= GROUP_C1_END) {
          handleC1Command(command);
          // Cues are always updated after a C1 command
          cues = getDisplayCues();
        } else if (command <= GROUP_G1_END) {
          handleG1Character(command);
        } else {
          Log.w(TAG, "Invalid base command: " + command);
        }
      } else {
        // Read the extended command
        command = serviceBlockPacket.readBits(8);
        if (command <= GROUP_C2_END) {
          handleC2Command(command);
        } else if (command <= GROUP_G2_END) {
          handleG2Character(command);
        } else if (command <= GROUP_C3_END) {
          handleC3Command(command);
        } else if (command <= GROUP_G3_END) {
          handleG3Character(command);
        } else {
          Log.w(TAG, "Invalid extended command: " + command);
        }
      }
    }
  }

  private void handleC0Command(int command) {
    switch (command) {
      case COMMAND_NUL:
        // Do nothing.
        break;
      case COMMAND_ETX:
        cues = getDisplayCues();
        break;
      case COMMAND_BS:
        currentCueBuilder.backspace();
        break;
      case COMMAND_FF:
        resetCueBuilders();
        break;
      case COMMAND_CR:
        currentCueBuilder.append('\n');
        break;
      case COMMAND_HCR:
        // TODO: Add support for this command.
        break;
      default:
        if (command >= COMMAND_EXT1_START && command <= COMMAND_EXT1_END) {
          Log.w(TAG, "Currently unsupported COMMAND_EXT1 Command: " + command);
          serviceBlockPacket.skipBits(8);
        } else if (command >= COMMAND_P16_START && command <= COMMAND_P16_END) {
          Log.w(TAG, "Currently unsupported COMMAND_P16 Command: " + command);
          serviceBlockPacket.skipBits(16);
        } else {
          Log.w(TAG, "Invalid C0 command: " + command);
        }
    }
  }

  private void handleC1Command(int command) {
    int window;
    switch (command) {
      case COMMAND_CW0:
      case COMMAND_CW1:
      case COMMAND_CW2:
      case COMMAND_CW3:
      case COMMAND_CW4:
      case COMMAND_CW5:
      case COMMAND_CW6:
      case COMMAND_CW7:
        window = (command - COMMAND_CW0);
        if (currentWindow != window) {
          currentWindow = window;
          currentCueBuilder = cueBuilders[window];
        }
        break;
      case COMMAND_CLW:
        for (int i = 1; i <= NUM_WINDOWS; i++) {
          if (serviceBlockPacket.readBit()) {
            cueBuilders[NUM_WINDOWS - i].clear();
          }
        }
        break;
      case COMMAND_DSW:
        for (int i = 1; i <= NUM_WINDOWS; i++) {
          if (serviceBlockPacket.readBit()) {
            cueBuilders[NUM_WINDOWS - i].setVisibility(true);
          }
        }
        break;
      case COMMAND_HDW:
        for (int i = 1; i <= NUM_WINDOWS; i++) {
          if (serviceBlockPacket.readBit()) {
            cueBuilders[NUM_WINDOWS - i].setVisibility(false);
          }
        }
        break;
      case COMMAND_TGW:
        for (int i = 1; i <= NUM_WINDOWS; i++) {
          if (serviceBlockPacket.readBit()) {
            CueBuilder cueBuilder = cueBuilders[NUM_WINDOWS - i];
            cueBuilder.setVisibility(!cueBuilder.isVisible());
          }
        }
        break;
      case COMMAND_DLW:
        for (int i = 1; i <= NUM_WINDOWS; i++) {
          if (serviceBlockPacket.readBit()) {
            cueBuilders[NUM_WINDOWS - i].reset();
          }
        }
        break;
      case COMMAND_DLY:
        // TODO: Add support for delay commands.
        serviceBlockPacket.skipBits(8);
        break;
      case COMMAND_DLC:
        // TODO: Add support for delay commands.
        break;
      case COMMAND_RST:
        resetCueBuilders();
        break;
      case COMMAND_SPA:
        if (!currentCueBuilder.isDefined()) {
          // ignore this command if the current window/cue isn't defined
          serviceBlockPacket.skipBits(16);
        } else {
          handleSetPenAttributes();
        }
        break;
      case COMMAND_SPC:
        if (!currentCueBuilder.isDefined()) {
          // ignore this command if the current window/cue isn't defined
          serviceBlockPacket.skipBits(24);
        } else {
          handleSetPenColor();
        }
        break;
      case COMMAND_SPL:
        if (!currentCueBuilder.isDefined()) {
          // ignore this command if the current window/cue isn't defined
          serviceBlockPacket.skipBits(16);
        } else {
          handleSetPenLocation();
        }
        break;
      case COMMAND_SWA:
        if (!currentCueBuilder.isDefined()) {
          // ignore this command if the current window/cue isn't defined
          serviceBlockPacket.skipBits(32);
        } else {
          handleSetWindowAttributes();
        }
        break;
      case COMMAND_DF0:
      case COMMAND_DF1:
      case COMMAND_DF2:
      case COMMAND_DF3:
      case COMMAND_DS4:
      case COMMAND_DF5:
      case COMMAND_DF6:
      case COMMAND_DF7:
        window = (command - COMMAND_DF0);
        handleDefineWindow(window);
        break;
      default:
        Log.w(TAG, "Invalid C1 command: " + command);
    }
  }

  private void handleC2Command(int command) {
    // C2 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
    if (command <= 0x0F) {
      // Do nothing.
    } else if (command <= 0x0F) {
      serviceBlockPacket.skipBits(8);
    } else if (command <= 0x17) {
      serviceBlockPacket.skipBits(16);
    } else if (command <= 0x1F) {
      serviceBlockPacket.skipBits(24);
    }
  }

  private void handleC3Command(int command) {
    // C3 Table doesn't contain any commands in CEA-708-B, but we do need to skip bytes
    if (command <= 0x87) {
      serviceBlockPacket.skipBits(32);
    } else if (command <= 0x8F) {
      serviceBlockPacket.skipBits(40);
    } else if (command <= 0x9F) {
      // 90-9F are variable length codes; the first byte defines the header with the first
      // 2 bits specifying the type and the last 6 bits specifying the remaining length of the
      // command in bytes
      serviceBlockPacket.skipBits(2);
      int length = serviceBlockPacket.readBits(6);
      serviceBlockPacket.skipBits(8 * length);
    }
  }

  private void handleG0Character(int characterCode) {
    if (characterCode == CHARACTER_MN) {
      currentCueBuilder.append('\u266B');
    } else {
      currentCueBuilder.append((char) (characterCode & 0xFF));
    }
  }

  private void handleG1Character(int characterCode) {
    currentCueBuilder.append((char) (characterCode & 0xFF));
  }

  private void handleG2Character(int characterCode) {
    switch (characterCode) {
      case CHARACTER_TSP:
        currentCueBuilder.append('\u0020');
        break;
      case CHARACTER_NBTSP:
        currentCueBuilder.append('\u00A0');
        break;
      case CHARACTER_ELLIPSIS:
        currentCueBuilder.append('\u2026');
        break;
      case CHARACTER_BIG_CARONS:
        currentCueBuilder.append('\u0160');
        break;
      case CHARACTER_BIG_OE:
        currentCueBuilder.append('\u0152');
        break;
      case CHARACTER_SOLID_BLOCK:
        currentCueBuilder.append('\u2588');
        break;
      case CHARACTER_OPEN_SINGLE_QUOTE:
        currentCueBuilder.append('\u2018');
        break;
      case CHARACTER_CLOSE_SINGLE_QUOTE:
        currentCueBuilder.append('\u2019');
        break;
      case CHARACTER_OPEN_DOUBLE_QUOTE:
        currentCueBuilder.append('\u201C');
        break;
      case CHARACTER_CLOSE_DOUBLE_QUOTE:
        currentCueBuilder.append('\u201D');
        break;
      case CHARACTER_BOLD_BULLET:
        currentCueBuilder.append('\u2022');
        break;
      case CHARACTER_TM:
        currentCueBuilder.append('\u2122');
        break;
      case CHARACTER_SMALL_CARONS:
        currentCueBuilder.append('\u0161');
        break;
      case CHARACTER_SMALL_OE:
        currentCueBuilder.append('\u0153');
        break;
      case CHARACTER_SM:
        currentCueBuilder.append('\u2120');
        break;
      case CHARACTER_DIAERESIS_Y:
        currentCueBuilder.append('\u0178');
        break;
      case CHARACTER_ONE_EIGHTH:
        currentCueBuilder.append('\u215B');
        break;
      case CHARACTER_THREE_EIGHTHS:
        currentCueBuilder.append('\u215C');
        break;
      case CHARACTER_FIVE_EIGHTHS:
        currentCueBuilder.append('\u215D');
        break;
      case CHARACTER_SEVEN_EIGHTHS:
        currentCueBuilder.append('\u215E');
        break;
      case CHARACTER_VERTICAL_BORDER:
        currentCueBuilder.append('\u2502');
        break;
      case CHARACTER_UPPER_RIGHT_BORDER:
        currentCueBuilder.append('\u2510');
        break;
      case CHARACTER_LOWER_LEFT_BORDER:
        currentCueBuilder.append('\u2514');
        break;
      case CHARACTER_HORIZONTAL_BORDER:
        currentCueBuilder.append('\u2500');
        break;
      case CHARACTER_LOWER_RIGHT_BORDER:
        currentCueBuilder.append('\u2518');
        break;
      case CHARACTER_UPPER_LEFT_BORDER:
        currentCueBuilder.append('\u250C');
        break;
      default:
        Log.w(TAG, "Invalid G2 character: " + characterCode);
        // The CEA-708 specification doesn't specify what to do in the case of an unexpected
        // value in the G2 character range, so we ignore it.
    }
  }

  private void handleG3Character(int characterCode) {
    if (characterCode == 0xA0) {
      currentCueBuilder.append('\u33C4');
    } else {
      Log.w(TAG, "Invalid G3 character: " + characterCode);
      // Substitute any unsupported G3 character with an underscore as per CEA-708 specification.
      currentCueBuilder.append('_');
    }
  }

  private void handleSetPenAttributes() {
    // the SetPenAttributes command contains 2 bytes of data
    // first byte
    int textTag = serviceBlockPacket.readBits(4);
    int offset = serviceBlockPacket.readBits(2);
    int penSize = serviceBlockPacket.readBits(2);
    // second byte
    boolean italicsToggle = serviceBlockPacket.readBit();
    boolean underlineToggle = serviceBlockPacket.readBit();
    int edgeType = serviceBlockPacket.readBits(3);
    int fontStyle = serviceBlockPacket.readBits(3);

    currentCueBuilder.setPenAttributes(textTag, offset, penSize, italicsToggle, underlineToggle,
        edgeType, fontStyle);
  }

  private void handleSetPenColor() {
    // the SetPenColor command contains 3 bytes of data
    // first byte
    int foregroundO = serviceBlockPacket.readBits(2);
    int foregroundR = serviceBlockPacket.readBits(2);
    int foregroundG = serviceBlockPacket.readBits(2);
    int foregroundB = serviceBlockPacket.readBits(2);
    int foregroundColor = CueBuilder.getArgbColorFromCeaColor(foregroundR, foregroundG, foregroundB,
        foregroundO);
    // second byte
    int backgroundO = serviceBlockPacket.readBits(2);
    int backgroundR = serviceBlockPacket.readBits(2);
    int backgroundG = serviceBlockPacket.readBits(2);
    int backgroundB = serviceBlockPacket.readBits(2);
    int backgroundColor = CueBuilder.getArgbColorFromCeaColor(backgroundR, backgroundG, backgroundB,
        backgroundO);
    // third byte
    serviceBlockPacket.skipBits(2); // null padding
    int edgeR = serviceBlockPacket.readBits(2);
    int edgeG = serviceBlockPacket.readBits(2);
    int edgeB = serviceBlockPacket.readBits(2);
    int edgeColor = CueBuilder.getArgbColorFromCeaColor(edgeR, edgeG, edgeB);

    currentCueBuilder.setPenColor(foregroundColor, backgroundColor, edgeColor);
  }

  private void handleSetPenLocation() {
    // the SetPenLocation command contains 2 bytes of data
    // first byte
    serviceBlockPacket.skipBits(4);
    int row = serviceBlockPacket.readBits(4);
    // second byte
    serviceBlockPacket.skipBits(2);
    int column = serviceBlockPacket.readBits(6);

    currentCueBuilder.setPenLocation(row, column);
  }

  private void handleSetWindowAttributes() {
    // the SetWindowAttributes command contains 4 bytes of data
    // first byte
    int fillO = serviceBlockPacket.readBits(2);
    int fillR = serviceBlockPacket.readBits(2);
    int fillG = serviceBlockPacket.readBits(2);
    int fillB = serviceBlockPacket.readBits(2);
    int fillColor = CueBuilder.getArgbColorFromCeaColor(fillR, fillG, fillB, fillO);
    // second byte
    int borderType = serviceBlockPacket.readBits(2); // only the lower 2 bits of borderType
    int borderR = serviceBlockPacket.readBits(2);
    int borderG = serviceBlockPacket.readBits(2);
    int borderB = serviceBlockPacket.readBits(2);
    int borderColor = CueBuilder.getArgbColorFromCeaColor(borderR, borderG, borderB);
    // third byte
    if (serviceBlockPacket.readBit()) {
      borderType |= 0x04; // set the top bit of the 3-bit borderType
    }
    boolean wordWrapToggle = serviceBlockPacket.readBit();
    int printDirection = serviceBlockPacket.readBits(2);
    int scrollDirection = serviceBlockPacket.readBits(2);
    int justification = serviceBlockPacket.readBits(2);
    // fourth byte
    // Note that we don't intend to support display effects
    serviceBlockPacket.skipBits(8); // effectSpeed(4), effectDirection(2), displayEffect(2)

    currentCueBuilder.setWindowAttributes(fillColor, borderColor, wordWrapToggle, borderType,
        printDirection, scrollDirection, justification);
  }

  private void handleDefineWindow(int window) {
    CueBuilder cueBuilder = cueBuilders[window];

    // the DefineWindow command contains 6 bytes of data
    // first byte
    serviceBlockPacket.skipBits(2); // null padding
    boolean visible = serviceBlockPacket.readBit();
    boolean rowLock = serviceBlockPacket.readBit();
    boolean columnLock = serviceBlockPacket.readBit();
    int priority = serviceBlockPacket.readBits(3);
    // second byte
    boolean relativePositioning = serviceBlockPacket.readBit();
    int verticalAnchor = serviceBlockPacket.readBits(7);
    // third byte
    int horizontalAnchor = serviceBlockPacket.readBits(8);
    // fourth byte
    int anchorId = serviceBlockPacket.readBits(4);
    int rowCount = serviceBlockPacket.readBits(4);
    // fifth byte
    serviceBlockPacket.skipBits(2); // null padding
    int columnCount = serviceBlockPacket.readBits(6);
    // sixth byte
    serviceBlockPacket.skipBits(2); // null padding
    int windowStyle = serviceBlockPacket.readBits(3);
    int penStyle = serviceBlockPacket.readBits(3);

    cueBuilder.defineWindow(visible, rowLock, columnLock, priority, relativePositioning,
        verticalAnchor, horizontalAnchor, rowCount, columnCount, anchorId, windowStyle, penStyle);
  }

  private List<Cue> getDisplayCues() {
    List<Cea708Cue> displayCues = new ArrayList<>();
    for (int i = 0; i < NUM_WINDOWS; i++) {
      if (!cueBuilders[i].isEmpty() && cueBuilders[i].isVisible()) {
        displayCues.add(cueBuilders[i].build());
      }
    }
    Collections.sort(displayCues);
    return Collections.<Cue>unmodifiableList(displayCues);
  }

  private void resetCueBuilders() {
    for (int i = 0; i < NUM_WINDOWS; i++) {
      cueBuilders[i].reset();
    }
  }

  private static final class DtvCcPacket {

    public final int sequenceNumber;
    public final int packetSize;
    public final byte[] packetData;

    int currentIndex;

    public DtvCcPacket(int sequenceNumber, int packetSize) {
      this.sequenceNumber = sequenceNumber;
      this.packetSize = packetSize;
      packetData = new byte[2 * packetSize - 1];
      currentIndex = 0;
    }

  }

  // TODO: There is a lot of overlap between Cea708Decoder.CueBuilder and Cea608Decoder.CueBuilder
  // which could be refactored into a separate class.
  private static final class CueBuilder {

    private static final int RELATIVE_CUE_SIZE = 99;
    private static final int VERTICAL_SIZE = 74;
    private static final int HORIZONTAL_SIZE = 209;

    private static final int DEFAULT_PRIORITY = 4;

    private static final int MAXIMUM_ROW_COUNT = 15;

    private static final int JUSTIFICATION_LEFT = 0;
    private static final int JUSTIFICATION_RIGHT = 1;
    private static final int JUSTIFICATION_CENTER = 2;
    private static final int JUSTIFICATION_FULL = 3;

    private static final int DIRECTION_LEFT_TO_RIGHT = 0;
    private static final int DIRECTION_RIGHT_TO_LEFT = 1;
    private static final int DIRECTION_TOP_TO_BOTTOM = 2;
    private static final int DIRECTION_BOTTOM_TO_TOP = 3;

    // TODO: Add other border/edge types when utilized.
    private static final int BORDER_AND_EDGE_TYPE_NONE = 0;
    private static final int BORDER_AND_EDGE_TYPE_UNIFORM = 3;

    public static final int COLOR_SOLID_WHITE = getArgbColorFromCeaColor(2, 2, 2, 0);
    public static final int COLOR_SOLID_BLACK = getArgbColorFromCeaColor(0, 0, 0, 0);
    public static final int COLOR_TRANSPARENT = getArgbColorFromCeaColor(0, 0, 0, 3);

    // TODO: Add other sizes when utilized.
    private static final int PEN_SIZE_STANDARD = 1;

    // TODO: Add other pen font styles when utilized.
    private static final int PEN_FONT_STYLE_DEFAULT = 0;
    private static final int PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS = 1;
    private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS = 2;
    private static final int PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS = 3;
    private static final int PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS = 4;

    // TODO: Add other pen offsets when utilized.
    private static final int PEN_OFFSET_NORMAL = 1;

    // The window style properties are specified in the CEA-708 specification.
    private static final int[] WINDOW_STYLE_JUSTIFICATION = new int[]{
        JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_LEFT,
        JUSTIFICATION_LEFT, JUSTIFICATION_LEFT, JUSTIFICATION_CENTER,
        JUSTIFICATION_LEFT
    };
    private static final int[] WINDOW_STYLE_PRINT_DIRECTION = new int[]{
        DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
        DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT, DIRECTION_LEFT_TO_RIGHT,
        DIRECTION_TOP_TO_BOTTOM
    };
    private static final int[] WINDOW_STYLE_SCROLL_DIRECTION = new int[]{
        DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
        DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP, DIRECTION_BOTTOM_TO_TOP,
        DIRECTION_RIGHT_TO_LEFT
    };
    private static final boolean[] WINDOW_STYLE_WORD_WRAP = new boolean[]{
        false, false, false, true, true, true, false
    };
    private static final int[] WINDOW_STYLE_FILL = new int[]{
        COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
        COLOR_TRANSPARENT, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK
    };

    // The pen style properties are specified in the CEA-708 specification.
    private static final int[] PEN_STYLE_FONT_STYLE = new int[]{
        PEN_FONT_STYLE_DEFAULT, PEN_FONT_STYLE_MONOSPACED_WITH_SERIFS,
        PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITH_SERIFS, PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
        PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS,
        PEN_FONT_STYLE_MONOSPACED_WITHOUT_SERIFS,
        PEN_FONT_STYLE_PROPORTIONALLY_SPACED_WITHOUT_SERIFS
    };
    private static final int[] PEN_STYLE_EDGE_TYPE = new int[]{
        BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE,
        BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_NONE, BORDER_AND_EDGE_TYPE_UNIFORM,
        BORDER_AND_EDGE_TYPE_UNIFORM
    };
    private static final int[] PEN_STYLE_BACKGROUND = new int[]{
        COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK, COLOR_SOLID_BLACK,
        COLOR_SOLID_BLACK, COLOR_TRANSPARENT, COLOR_TRANSPARENT};

    private final List<SpannableString> rolledUpCaptions;
    private final SpannableStringBuilder captionStringBuilder;

    // Window/Cue properties
    private boolean defined;
    private boolean visible;
    private int priority;
    private boolean relativePositioning;
    private int verticalAnchor;
    private int horizontalAnchor;
    private int anchorId;
    private int rowCount;
    private boolean rowLock;
    private int justification;
    private int windowStyleId;
    private int penStyleId;
    private int windowFillColor;

    // Pen/Text properties
    private int italicsStartPosition;
    private int underlineStartPosition;
    private int foregroundColorStartPosition;
    private int foregroundColor;
    private int backgroundColorStartPosition;
    private int backgroundColor;

    public CueBuilder() {
      rolledUpCaptions = new LinkedList<>();
      captionStringBuilder = new SpannableStringBuilder();
      reset();
    }

    public boolean isEmpty() {
      return !isDefined() || (rolledUpCaptions.isEmpty() && captionStringBuilder.length() == 0);
    }

    public void reset() {
      clear();

      defined = false;
      visible = false;
      priority = DEFAULT_PRIORITY;
      relativePositioning = false;
      verticalAnchor = 0;
      horizontalAnchor = 0;
      anchorId = 0;
      rowCount = MAXIMUM_ROW_COUNT;
      rowLock = true;
      justification = JUSTIFICATION_LEFT;
      windowStyleId = 0;
      penStyleId = 0;
      windowFillColor = COLOR_SOLID_BLACK;

      foregroundColor = COLOR_SOLID_WHITE;
      backgroundColor = COLOR_SOLID_BLACK;
    }

    public void clear() {
      rolledUpCaptions.clear();
      captionStringBuilder.clear();
      italicsStartPosition = C.POSITION_UNSET;
      underlineStartPosition = C.POSITION_UNSET;
      foregroundColorStartPosition = C.POSITION_UNSET;
      backgroundColorStartPosition = C.POSITION_UNSET;
    }

    public boolean isDefined() {
      return defined;
    }

    public void setVisibility(boolean visible) {
      this.visible = visible;
    }

    public boolean isVisible() {
      return visible;
    }

    public void defineWindow(boolean visible, boolean rowLock, boolean columnLock, int priority,
        boolean relativePositioning, int verticalAnchor, int horizontalAnchor, int rowCount,
        int columnCount, int anchorId, int windowStyleId, int penStyleId) {
      this.defined = true;
      this.visible = visible;
      this.rowLock = rowLock;
      this.priority = priority;
      this.relativePositioning = relativePositioning;
      this.verticalAnchor = verticalAnchor;
      this.horizontalAnchor = horizontalAnchor;
      this.anchorId = anchorId;

      // Decoders must add one to rowCount to get the desired number of rows.
      if (this.rowCount != rowCount + 1) {
        this.rowCount = rowCount + 1;

        // Trim any rolled up captions that are no longer valid, if applicable.
        while ((rowLock && (rolledUpCaptions.size() >= this.rowCount))
            || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
          rolledUpCaptions.remove(0);
        }
      }

      // TODO: Add support for column lock and count.

      if (windowStyleId != 0 && this.windowStyleId != windowStyleId) {
        this.windowStyleId = windowStyleId;
        // windowStyleId is 1-based.
        int windowStyleIdIndex = windowStyleId - 1;
        // Note that Border type and border color are the same for all window styles.
        setWindowAttributes(WINDOW_STYLE_FILL[windowStyleIdIndex], COLOR_TRANSPARENT,
            WINDOW_STYLE_WORD_WRAP[windowStyleIdIndex], BORDER_AND_EDGE_TYPE_NONE,
            WINDOW_STYLE_PRINT_DIRECTION[windowStyleIdIndex],
            WINDOW_STYLE_SCROLL_DIRECTION[windowStyleIdIndex],
            WINDOW_STYLE_JUSTIFICATION[windowStyleIdIndex]);
      }

      if (penStyleId != 0 && this.penStyleId != penStyleId) {
        this.penStyleId = penStyleId;
        // penStyleId is 1-based.
        int penStyleIdIndex = penStyleId - 1;
        // Note that pen size, offset, italics, underline, foreground color, and foreground
        // opacity are the same for all pen styles.
        setPenAttributes(0, PEN_OFFSET_NORMAL, PEN_SIZE_STANDARD, false, false,
            PEN_STYLE_EDGE_TYPE[penStyleIdIndex], PEN_STYLE_FONT_STYLE[penStyleIdIndex]);
        setPenColor(COLOR_SOLID_WHITE, PEN_STYLE_BACKGROUND[penStyleIdIndex], COLOR_SOLID_BLACK);
      }
    }


    public void setWindowAttributes(int fillColor, int borderColor, boolean wordWrapToggle,
        int borderType, int printDirection, int scrollDirection, int justification) {
      this.windowFillColor = fillColor;
      // TODO: Add support for border color and types.
      // TODO: Add support for word wrap.
      // TODO: Add support for other scroll directions.
      // TODO: Add support for other print directions.
      this.justification = justification;

    }

    public void setPenAttributes(int textTag, int offset, int penSize, boolean italicsToggle,
        boolean underlineToggle, int edgeType, int fontStyle) {
      // TODO: Add support for text tags.
      // TODO: Add support for other offsets.
      // TODO: Add support for other pen sizes.

      if (italicsStartPosition != C.POSITION_UNSET) {
        if (!italicsToggle) {
          captionStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
              captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          italicsStartPosition = C.POSITION_UNSET;
        }
      } else if (italicsToggle) {
        italicsStartPosition = captionStringBuilder.length();
      }

      if (underlineStartPosition != C.POSITION_UNSET) {
        if (!underlineToggle) {
          captionStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
              captionStringBuilder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
          underlineStartPosition = C.POSITION_UNSET;
        }
      } else if (underlineToggle) {
        underlineStartPosition = captionStringBuilder.length();
      }

      // TODO: Add support for edge types.
      // TODO: Add support for other font styles.
    }

    public void setPenColor(int foregroundColor, int backgroundColor, int edgeColor) {
      if (foregroundColorStartPosition != C.POSITION_UNSET) {
        if (this.foregroundColor != foregroundColor) {
          captionStringBuilder.setSpan(new ForegroundColorSpan(this.foregroundColor),
              foregroundColorStartPosition, captionStringBuilder.length(),
              Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
      }
      if (foregroundColor != COLOR_SOLID_WHITE) {
        foregroundColorStartPosition = captionStringBuilder.length();
        this.foregroundColor = foregroundColor;
      }

      if (backgroundColorStartPosition != C.POSITION_UNSET) {
        if (this.backgroundColor != backgroundColor) {
          captionStringBuilder.setSpan(new BackgroundColorSpan(this.backgroundColor),
              backgroundColorStartPosition, captionStringBuilder.length(),
              Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
      }
      if (backgroundColor != COLOR_SOLID_BLACK) {
        backgroundColorStartPosition = captionStringBuilder.length();
        this.backgroundColor = backgroundColor;
      }

      // TODO: Add support for edge color.
    }

    public void setPenLocation(int row, int column) {
      // TODO: Support moving the pen location with a window.
    }

    public void backspace() {
      int length = captionStringBuilder.length();
      if (length > 0) {
        captionStringBuilder.delete(length - 1, length);
      }
    }

    public void append(char text) {
      if (text == '\n') {
        rolledUpCaptions.add(buildSpannableString());
        captionStringBuilder.clear();

        if (italicsStartPosition != C.POSITION_UNSET) {
          italicsStartPosition = 0;
        }
        if (underlineStartPosition != C.POSITION_UNSET) {
          underlineStartPosition = 0;
        }
        if (foregroundColorStartPosition != C.POSITION_UNSET) {
          foregroundColorStartPosition = 0;
        }
        if (backgroundColorStartPosition != C.POSITION_UNSET) {
          backgroundColorStartPosition = 0;
        }

        while ((rowLock && (rolledUpCaptions.size() >= rowCount))
            || (rolledUpCaptions.size() >= MAXIMUM_ROW_COUNT)) {
          rolledUpCaptions.remove(0);
        }
      } else {
        captionStringBuilder.append(text);
      }
    }

    public SpannableString buildSpannableString() {
      SpannableStringBuilder spannableStringBuilder =
          new SpannableStringBuilder(captionStringBuilder);
      int length = spannableStringBuilder.length();

      if (length > 0) {
        if (italicsStartPosition != C.POSITION_UNSET) {
          spannableStringBuilder.setSpan(new StyleSpan(Typeface.ITALIC), italicsStartPosition,
              length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        if (underlineStartPosition != C.POSITION_UNSET) {
          spannableStringBuilder.setSpan(new UnderlineSpan(), underlineStartPosition,
              length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        if (foregroundColorStartPosition != C.POSITION_UNSET) {
          spannableStringBuilder.setSpan(new ForegroundColorSpan(foregroundColor),
              foregroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        if (backgroundColorStartPosition != C.POSITION_UNSET) {
          spannableStringBuilder.setSpan(new BackgroundColorSpan(backgroundColor),
              backgroundColorStartPosition, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
      }

      return new SpannableString(spannableStringBuilder);
    }

    public Cea708Cue build() {
      if (isEmpty()) {
        // The cue is empty.
        return null;
      }

      SpannableStringBuilder cueString = new SpannableStringBuilder();

      // Add any rolled up captions, separated by new lines.
      for (int i = 0; i < rolledUpCaptions.size(); i++) {
        cueString.append(rolledUpCaptions.get(i));
        cueString.append('\n');
      }
      // Add the current line.
      cueString.append(buildSpannableString());

      // TODO: Add support for right-to-left languages (i.e. where right would correspond to normal
      // alignment).
      Alignment alignment;
      switch (justification) {
        case JUSTIFICATION_FULL:
          // TODO: Add support for full justification.
        case JUSTIFICATION_LEFT:
          alignment = Alignment.ALIGN_NORMAL;
          break;
        case JUSTIFICATION_RIGHT:
          alignment = Alignment.ALIGN_OPPOSITE;
          break;
        case JUSTIFICATION_CENTER:
          alignment = Alignment.ALIGN_CENTER;
          break;
        default:
          throw new IllegalArgumentException("Unexpected justification value: " + justification);
      }

      float position;
      float line;
      if (relativePositioning) {
        position = (float) horizontalAnchor / RELATIVE_CUE_SIZE;
        line = (float) verticalAnchor / RELATIVE_CUE_SIZE;
      } else {
        position = (float) horizontalAnchor / HORIZONTAL_SIZE;
        line = (float) verticalAnchor / VERTICAL_SIZE;
      }
      // Apply screen-edge padding to the line and position.
      position = (position * 0.9f) + 0.05f;
      line = (line * 0.9f) + 0.05f;

      // anchorId specifies where the anchor should be placed on the caption cue/window. The 9
      // possible configurations are as follows:
      //   0-----1-----2
      //   |           |
      //   3     4     5
      //   |           |
      //   6-----7-----8
      @AnchorType int verticalAnchorType;
      if (anchorId % 3 == 0) {
        verticalAnchorType = Cue.ANCHOR_TYPE_START;
      } else if (anchorId % 3 == 1) {
        verticalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
      } else {
        verticalAnchorType = Cue.ANCHOR_TYPE_END;
      }
      // TODO: Add support for right-to-left languages (i.e. where start is on the right).
      @AnchorType int horizontalAnchorType;
      if (anchorId / 3 == 0) {
        horizontalAnchorType = Cue.ANCHOR_TYPE_START;
      } else if (anchorId / 3 == 1) {
        horizontalAnchorType = Cue.ANCHOR_TYPE_MIDDLE;
      } else {
        horizontalAnchorType = Cue.ANCHOR_TYPE_END;
      }

      boolean windowColorSet = (windowFillColor != COLOR_SOLID_BLACK);

      return new Cea708Cue(cueString, alignment, line, Cue.LINE_TYPE_FRACTION, verticalAnchorType,
          position, horizontalAnchorType, Cue.DIMEN_UNSET, windowColorSet, windowFillColor,
          priority);
    }

    public static int getArgbColorFromCeaColor(int red, int green, int blue) {
      return getArgbColorFromCeaColor(red, green, blue, 0);
    }

    public static int getArgbColorFromCeaColor(int red, int green, int blue, int opacity) {
      Assertions.checkIndex(red, 0, 4);
      Assertions.checkIndex(green, 0, 4);
      Assertions.checkIndex(blue, 0, 4);
      Assertions.checkIndex(opacity, 0, 4);

      int alpha;
      switch (opacity) {
        case 0:
        case 1:
          // Note the value of '1' is actually FLASH, but we don't support that.
          alpha = 255;
          break;
        case 2:
          alpha = 127;
          break;
        case 3:
          alpha = 0;
          break;
        default:
          alpha = 255;
      }

      // TODO: Add support for the Alternative Minimum Color List or the full 64 RGB combinations.

      // Return values based on the Minimum Color List
      return Color.argb(alpha,
          (red > 1 ? 255 : 0),
          (green > 1 ? 255 : 0),
          (blue > 1 ? 255 : 0));
    }

  }

}
