/*
 * Copyright 2018 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.android.media.subtitle;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.media.MediaFormat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.text.style.UpdateAppearance;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.CaptioningManager;
import android.view.accessibility.CaptioningManager.CaptionStyle;
import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
import android.widget.LinearLayout;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Vector;

// Note: This is forked from android.media.ClosedCaptionRenderer since P
public class ClosedCaptionRenderer extends SubtitleController.Renderer {
    private final Context mContext;
    private Cea608CCWidget mCCWidget;

    public ClosedCaptionRenderer(Context context) {
        mContext = context;
    }

    @Override
    public boolean supports(MediaFormat format) {
        if (format.containsKey(MediaFormat.KEY_MIME)) {
            String mimeType = format.getString(MediaFormat.KEY_MIME);
            return MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType);
        }
        return false;
    }

    @Override
    public SubtitleTrack createTrack(MediaFormat format) {
        String mimeType = format.getString(MediaFormat.KEY_MIME);
        if (MediaFormat.MIMETYPE_TEXT_CEA_608.equals(mimeType)) {
            if (mCCWidget == null) {
                mCCWidget = new Cea608CCWidget(mContext);
            }
            return new Cea608CaptionTrack(mCCWidget, format);
        }
        throw new RuntimeException("No matching format: " + format.toString());
    }
}

class Cea608CaptionTrack extends SubtitleTrack {
    private final Cea608CCParser mCCParser;
    private final Cea608CCWidget mRenderingWidget;

    Cea608CaptionTrack(Cea608CCWidget renderingWidget, MediaFormat format) {
        super(format);

        mRenderingWidget = renderingWidget;
        mCCParser = new Cea608CCParser(mRenderingWidget);
    }

    @Override
    public void onData(byte[] data, boolean eos, long runID) {
        mCCParser.parse(data);
    }

    @Override
    public RenderingWidget getRenderingWidget() {
        return mRenderingWidget;
    }

    @Override
    public void updateView(Vector<Cue> activeCues) {
        // Overriding with NO-OP, CC rendering by-passes this
    }
}

/**
 * Abstract widget class to render a closed caption track.
 */
abstract class ClosedCaptionWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {

    interface ClosedCaptionLayout {
        void setCaptionStyle(CaptionStyle captionStyle);
        void setFontScale(float scale);
    }

    private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT;

    /** Captioning manager, used to obtain and track caption properties. */
    private final CaptioningManager mManager;

    /** Current caption style. */
    protected CaptionStyle mCaptionStyle;

    /** Callback for rendering changes. */
    protected OnChangedListener mListener;

    /** Concrete layout of CC. */
    protected ClosedCaptionLayout mClosedCaptionLayout;

    /** Whether a caption style change listener is registered. */
    private boolean mHasChangeListener;

    public ClosedCaptionWidget(Context context) {
        this(context, null);
    }

    public ClosedCaptionWidget(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyle) {
        this(context, attrs, defStyle, 0);
    }

    public ClosedCaptionWidget(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        // Cannot render text over video when layer type is hardware.
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
        mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(mManager.getUserStyle());

        mClosedCaptionLayout = createCaptionLayout(context);
        mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
        mClosedCaptionLayout.setFontScale(mManager.getFontScale());
        addView((ViewGroup) mClosedCaptionLayout, LayoutParams.MATCH_PARENT,
                LayoutParams.MATCH_PARENT);

        requestLayout();
    }

    public abstract ClosedCaptionLayout createCaptionLayout(Context context);

    @Override
    public void setOnChangedListener(OnChangedListener listener) {
        mListener = listener;
    }

    @Override
    public void setSize(int width, int height) {
        final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

        measure(widthSpec, heightSpec);
        layout(0, 0, width, height);
    }

    @Override
    public void setVisible(boolean visible) {
        if (visible) {
            setVisibility(View.VISIBLE);
        } else {
            setVisibility(View.GONE);
        }

        manageChangeListener();
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();

        manageChangeListener();
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        manageChangeListener();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ((ViewGroup) mClosedCaptionLayout).measure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        ((ViewGroup) mClosedCaptionLayout).layout(l, t, r, b);
    }

    /**
     * Manages whether this renderer is listening for caption style changes.
     */
    private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
        @Override
        public void onUserStyleChanged(CaptionStyle userStyle) {
            mCaptionStyle = DEFAULT_CAPTION_STYLE.applyStyle(userStyle);
            mClosedCaptionLayout.setCaptionStyle(mCaptionStyle);
        }

        @Override
        public void onFontScaleChanged(float fontScale) {
            mClosedCaptionLayout.setFontScale(fontScale);
        }
    };

    private void manageChangeListener() {
        final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
        if (mHasChangeListener != needsListener) {
            mHasChangeListener = needsListener;

            if (needsListener) {
                mManager.addCaptioningChangeListener(mCaptioningListener);
            } else {
                mManager.removeCaptioningChangeListener(mCaptioningListener);
            }
        }
    }
}

/**
 * CCParser processes CEA-608 closed caption data.
 *
 * It calls back into OnDisplayChangedListener upon
 * display change with styled text for rendering.
 *
 */
class Cea608CCParser {
    public static final int MAX_ROWS = 15;
    public static final int MAX_COLS = 32;

    private static final String TAG = "Cea608CCParser";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private static final int INVALID = -1;

    // EIA-CEA-608: Table 70 - Control Codes
    private static final int RCL = 0x20;
    private static final int BS  = 0x21;
    private static final int AOF = 0x22;
    private static final int AON = 0x23;
    private static final int DER = 0x24;
    private static final int RU2 = 0x25;
    private static final int RU3 = 0x26;
    private static final int RU4 = 0x27;
    private static final int FON = 0x28;
    private static final int RDC = 0x29;
    private static final int TR  = 0x2a;
    private static final int RTD = 0x2b;
    private static final int EDM = 0x2c;
    private static final int CR  = 0x2d;
    private static final int ENM = 0x2e;
    private static final int EOC = 0x2f;

    // Transparent Space
    private static final char TS = '\u00A0';

    // Captioning Modes
    private static final int MODE_UNKNOWN = 0;
    private static final int MODE_PAINT_ON = 1;
    private static final int MODE_ROLL_UP = 2;
    private static final int MODE_POP_ON = 3;
    private static final int MODE_TEXT = 4;

    private final DisplayListener mListener;

    private int mMode = MODE_PAINT_ON;
    private int mRollUpSize = 4;
    private int mPrevCtrlCode = INVALID;

    private CCMemory mDisplay = new CCMemory();
    private CCMemory mNonDisplay = new CCMemory();
    private CCMemory mTextMem = new CCMemory();

    Cea608CCParser(DisplayListener listener) {
        mListener = listener;
    }

    public void parse(byte[] data) {
        CCData[] ccData = CCData.fromByteArray(data);

        for (int i = 0; i < ccData.length; i++) {
            if (DEBUG) {
                Log.d(TAG, ccData[i].toString());
            }

            if (handleCtrlCode(ccData[i])
                    || handleTabOffsets(ccData[i])
                    || handlePACCode(ccData[i])
                    || handleMidRowCode(ccData[i])) {
                continue;
            }

            handleDisplayableChars(ccData[i]);
        }
    }

    interface DisplayListener {
        void onDisplayChanged(SpannableStringBuilder[] styledTexts);
        CaptionStyle getCaptionStyle();
    }

    private CCMemory getMemory() {
        // get the CC memory to operate on for current mode
        switch (mMode) {
        case MODE_POP_ON:
            return mNonDisplay;
        case MODE_TEXT:
            // TODO(chz): support only caption mode for now,
            // in text mode, dump everything to text mem.
            return mTextMem;
        case MODE_PAINT_ON:
        case MODE_ROLL_UP:
            return mDisplay;
        default:
            Log.w(TAG, "unrecoginized mode: " + mMode);
        }
        return mDisplay;
    }

    private boolean handleDisplayableChars(CCData ccData) {
        if (!ccData.isDisplayableChar()) {
            return false;
        }

        // Extended char includes 1 automatic backspace
        if (ccData.isExtendedChar()) {
            getMemory().bs();
        }

        getMemory().writeText(ccData.getDisplayText());

        if (mMode == MODE_PAINT_ON || mMode == MODE_ROLL_UP) {
            updateDisplay();
        }

        return true;
    }

    private boolean handleMidRowCode(CCData ccData) {
        StyleCode m = ccData.getMidRow();
        if (m != null) {
            getMemory().writeMidRowCode(m);
            return true;
        }
        return false;
    }

    private boolean handlePACCode(CCData ccData) {
        PAC pac = ccData.getPAC();

        if (pac != null) {
            if (mMode == MODE_ROLL_UP) {
                getMemory().moveBaselineTo(pac.getRow(), mRollUpSize);
            }
            getMemory().writePAC(pac);
            return true;
        }

        return false;
    }

    private boolean handleTabOffsets(CCData ccData) {
        int tabs = ccData.getTabOffset();

        if (tabs > 0) {
            getMemory().tab(tabs);
            return true;
        }

        return false;
    }

    private boolean handleCtrlCode(CCData ccData) {
        int ctrlCode = ccData.getCtrlCode();

        if (mPrevCtrlCode != INVALID && mPrevCtrlCode == ctrlCode) {
            // discard double ctrl codes (but if there's a 3rd one, we still take that)
            mPrevCtrlCode = INVALID;
            return true;
        }

        switch(ctrlCode) {
        case RCL:
            // select pop-on style
            mMode = MODE_POP_ON;
            break;
        case BS:
            getMemory().bs();
            break;
        case DER:
            getMemory().der();
            break;
        case RU2:
        case RU3:
        case RU4:
            mRollUpSize = (ctrlCode - 0x23);
            // erase memory if currently in other style
            if (mMode != MODE_ROLL_UP) {
                mDisplay.erase();
                mNonDisplay.erase();
            }
            // select roll-up style
            mMode = MODE_ROLL_UP;
            break;
        case FON:
            Log.i(TAG, "Flash On");
            break;
        case RDC:
            // select paint-on style
            mMode = MODE_PAINT_ON;
            break;
        case TR:
            mMode = MODE_TEXT;
            mTextMem.erase();
            break;
        case RTD:
            mMode = MODE_TEXT;
            break;
        case EDM:
            // erase display memory
            mDisplay.erase();
            updateDisplay();
            break;
        case CR:
            if (mMode == MODE_ROLL_UP) {
                getMemory().rollUp(mRollUpSize);
            } else {
                getMemory().cr();
            }
            if (mMode == MODE_ROLL_UP) {
                updateDisplay();
            }
            break;
        case ENM:
            // erase non-display memory
            mNonDisplay.erase();
            break;
        case EOC:
            // swap display/non-display memory
            swapMemory();
            // switch to pop-on style
            mMode = MODE_POP_ON;
            updateDisplay();
            break;
        case INVALID:
        default:
            mPrevCtrlCode = INVALID;
            return false;
        }

        mPrevCtrlCode = ctrlCode;

        // handled
        return true;
    }

    private void updateDisplay() {
        if (mListener != null) {
            CaptionStyle captionStyle = mListener.getCaptionStyle();
            mListener.onDisplayChanged(mDisplay.getStyledText(captionStyle));
        }
    }

    private void swapMemory() {
        CCMemory temp = mDisplay;
        mDisplay = mNonDisplay;
        mNonDisplay = temp;
    }

    private static class StyleCode {
        static final int COLOR_WHITE = 0;
        static final int COLOR_GREEN = 1;
        static final int COLOR_BLUE = 2;
        static final int COLOR_CYAN = 3;
        static final int COLOR_RED = 4;
        static final int COLOR_YELLOW = 5;
        static final int COLOR_MAGENTA = 6;
        static final int COLOR_INVALID = 7;

        static final int STYLE_ITALICS   = 0x00000001;
        static final int STYLE_UNDERLINE = 0x00000002;

        static final String[] mColorMap = {
            "WHITE", "GREEN", "BLUE", "CYAN", "RED", "YELLOW", "MAGENTA", "INVALID"
        };

        final int mStyle;
        final int mColor;

        static StyleCode fromByte(byte data2) {
            int style = 0;
            int color = (data2 >> 1) & 0x7;

            if ((data2 & 0x1) != 0) {
                style |= STYLE_UNDERLINE;
            }

            if (color == COLOR_INVALID) {
                // WHITE ITALICS
                color = COLOR_WHITE;
                style |= STYLE_ITALICS;
            }

            return new StyleCode(style, color);
        }

        StyleCode(int style, int color) {
            mStyle = style;
            mColor = color;
        }

        boolean isItalics() {
            return (mStyle & STYLE_ITALICS) != 0;
        }

        boolean isUnderline() {
            return (mStyle & STYLE_UNDERLINE) != 0;
        }

        int getColor() {
            return mColor;
        }

        @Override
        public String toString() {
            StringBuilder str = new StringBuilder();
            str.append("{");
            str.append(mColorMap[mColor]);
            if ((mStyle & STYLE_ITALICS) != 0) {
                str.append(", ITALICS");
            }
            if ((mStyle & STYLE_UNDERLINE) != 0) {
                str.append(", UNDERLINE");
            }
            str.append("}");

            return str.toString();
        }
    }

    private static class PAC extends StyleCode {
        final int mRow;
        final int mCol;

        static PAC fromBytes(byte data1, byte data2) {
            int[] rowTable = {11, 1, 3, 12, 14, 5, 7, 9};
            int row = rowTable[data1 & 0x07] + ((data2 & 0x20) >> 5);
            int style = 0;
            if ((data2 & 1) != 0) {
                style |= STYLE_UNDERLINE;
            }
            if ((data2 & 0x10) != 0) {
                // indent code
                int indent = (data2 >> 1) & 0x7;
                return new PAC(row, indent * 4, style, COLOR_WHITE);
            } else {
                // style code
                int color = (data2 >> 1) & 0x7;

                if (color == COLOR_INVALID) {
                    // WHITE ITALICS
                    color = COLOR_WHITE;
                    style |= STYLE_ITALICS;
                }
                return new PAC(row, -1, style, color);
            }
        }

        PAC(int row, int col, int style, int color) {
            super(style, color);
            mRow = row;
            mCol = col;
        }

        boolean isIndentPAC() {
            return (mCol >= 0);
        }

        int getRow() {
            return mRow;
        }

        int getCol() {
            return mCol;
        }

        @Override
        public String toString() {
            return String.format("{%d, %d}, %s",
                    mRow, mCol, super.toString());
        }
    }

    /**
     * Mutable version of BackgroundSpan to facilitate text rendering with edge styles.
     */
    public static class MutableBackgroundColorSpan extends CharacterStyle
            implements UpdateAppearance {
        private int mColor;

        public MutableBackgroundColorSpan(int color) {
            mColor = color;
        }

        public void setBackgroundColor(int color) {
            mColor = color;
        }

        public int getBackgroundColor() {
            return mColor;
        }

        @Override
        public void updateDrawState(TextPaint ds) {
            ds.bgColor = mColor;
        }
    }

    /* CCLineBuilder keeps track of displayable chars, as well as
     * MidRow styles and PACs, for a single line of CC memory.
     *
     * It generates styled text via getStyledText() method.
     */
    private static class CCLineBuilder {
        private final StringBuilder mDisplayChars;
        private final StyleCode[] mMidRowStyles;
        private final StyleCode[] mPACStyles;

        CCLineBuilder(String str) {
            mDisplayChars = new StringBuilder(str);
            mMidRowStyles = new StyleCode[mDisplayChars.length()];
            mPACStyles = new StyleCode[mDisplayChars.length()];
        }

        void setCharAt(int index, char ch) {
            mDisplayChars.setCharAt(index, ch);
            mMidRowStyles[index] = null;
        }

        void setMidRowAt(int index, StyleCode m) {
            mDisplayChars.setCharAt(index, ' ');
            mMidRowStyles[index] = m;
        }

        void setPACAt(int index, PAC pac) {
            mPACStyles[index] = pac;
        }

        char charAt(int index) {
            return mDisplayChars.charAt(index);
        }

        int length() {
            return mDisplayChars.length();
        }

        void applyStyleSpan(
                SpannableStringBuilder styledText,
                StyleCode s, int start, int end) {
            if (s.isItalics()) {
                styledText.setSpan(
                        new StyleSpan(android.graphics.Typeface.ITALIC),
                        start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
            if (s.isUnderline()) {
                styledText.setSpan(
                        new UnderlineSpan(),
                        start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }

        SpannableStringBuilder getStyledText(CaptionStyle captionStyle) {
            SpannableStringBuilder styledText = new SpannableStringBuilder(mDisplayChars);
            int start = -1, next = 0;
            int styleStart = -1;
            StyleCode curStyle = null;
            while (next < mDisplayChars.length()) {
                StyleCode newStyle = null;
                if (mMidRowStyles[next] != null) {
                    // apply mid-row style change
                    newStyle = mMidRowStyles[next];
                } else if (mPACStyles[next] != null
                    && (styleStart < 0 || start < 0)) {
                    // apply PAC style change, only if:
                    // 1. no style set, or
                    // 2. style set, but prev char is none-displayable
                    newStyle = mPACStyles[next];
                }
                if (newStyle != null) {
                    curStyle = newStyle;
                    if (styleStart >= 0 && start >= 0) {
                        applyStyleSpan(styledText, newStyle, styleStart, next);
                    }
                    styleStart = next;
                }

                if (mDisplayChars.charAt(next) != TS) {
                    if (start < 0) {
                        start = next;
                    }
                } else if (start >= 0) {
                    int expandedStart = mDisplayChars.charAt(start) == ' ' ? start : start - 1;
                    int expandedEnd = mDisplayChars.charAt(next - 1) == ' ' ? next : next + 1;
                    styledText.setSpan(
                            new MutableBackgroundColorSpan(captionStyle.backgroundColor),
                            expandedStart, expandedEnd,
                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    if (styleStart >= 0) {
                        applyStyleSpan(styledText, curStyle, styleStart, expandedEnd);
                    }
                    start = -1;
                }
                next++;
            }

            return styledText;
        }
    }

    /*
     * CCMemory models a console-style display.
     */
    private static class CCMemory {
        private final String mBlankLine;
        private final CCLineBuilder[] mLines = new CCLineBuilder[MAX_ROWS + 2];
        private int mRow;
        private int mCol;

        CCMemory() {
            char[] blank = new char[MAX_COLS + 2];
            Arrays.fill(blank, TS);
            mBlankLine = new String(blank);
        }

        void erase() {
            // erase all lines
            for (int i = 0; i < mLines.length; i++) {
                mLines[i] = null;
            }
            mRow = MAX_ROWS;
            mCol = 1;
        }

        void der() {
            if (mLines[mRow] != null) {
                for (int i = 0; i < mCol; i++) {
                    if (mLines[mRow].charAt(i) != TS) {
                        for (int j = mCol; j < mLines[mRow].length(); j++) {
                            mLines[j].setCharAt(j, TS);
                        }
                        return;
                    }
                }
                mLines[mRow] = null;
            }
        }

        void tab(int tabs) {
            moveCursorByCol(tabs);
        }

        void bs() {
            moveCursorByCol(-1);
            if (mLines[mRow] != null) {
                mLines[mRow].setCharAt(mCol, TS);
                if (mCol == MAX_COLS - 1) {
                    // Spec recommendation:
                    // if cursor was at col 32, move cursor
                    // back to col 31 and erase both col 31&32
                    mLines[mRow].setCharAt(MAX_COLS, TS);
                }
            }
        }

        void cr() {
            moveCursorTo(mRow + 1, 1);
        }

        void rollUp(int windowSize) {
            int i;
            for (i = 0; i <= mRow - windowSize; i++) {
                mLines[i] = null;
            }
            int startRow = mRow - windowSize + 1;
            if (startRow < 1) {
                startRow = 1;
            }
            for (i = startRow; i < mRow; i++) {
                mLines[i] = mLines[i + 1];
            }
            for (i = mRow; i < mLines.length; i++) {
                // clear base row
                mLines[i] = null;
            }
            // default to col 1, in case PAC is not sent
            mCol = 1;
        }

        void writeText(String text) {
            for (int i = 0; i < text.length(); i++) {
                getLineBuffer(mRow).setCharAt(mCol, text.charAt(i));
                moveCursorByCol(1);
            }
        }

        void writeMidRowCode(StyleCode m) {
            getLineBuffer(mRow).setMidRowAt(mCol, m);
            moveCursorByCol(1);
        }

        void writePAC(PAC pac) {
            if (pac.isIndentPAC()) {
                moveCursorTo(pac.getRow(), pac.getCol());
            } else {
                moveCursorTo(pac.getRow(), 1);
            }
            getLineBuffer(mRow).setPACAt(mCol, pac);
        }

        SpannableStringBuilder[] getStyledText(CaptionStyle captionStyle) {
            ArrayList<SpannableStringBuilder> rows = new ArrayList<>(MAX_ROWS);
            for (int i = 1; i <= MAX_ROWS; i++) {
                rows.add(mLines[i] != null ?
                        mLines[i].getStyledText(captionStyle) : null);
            }
            return rows.toArray(new SpannableStringBuilder[MAX_ROWS]);
        }

        private static int clamp(int x, int min, int max) {
            return x < min ? min : (x > max ? max : x);
        }

        private void moveCursorTo(int row, int col) {
            mRow = clamp(row, 1, MAX_ROWS);
            mCol = clamp(col, 1, MAX_COLS);
        }

        private void moveCursorToRow(int row) {
            mRow = clamp(row, 1, MAX_ROWS);
        }

        private void moveCursorByCol(int col) {
            mCol = clamp(mCol + col, 1, MAX_COLS);
        }

        private void moveBaselineTo(int baseRow, int windowSize) {
            if (mRow == baseRow) {
                return;
            }
            int actualWindowSize = windowSize;
            if (baseRow < actualWindowSize) {
                actualWindowSize = baseRow;
            }
            if (mRow < actualWindowSize) {
                actualWindowSize = mRow;
            }

            int i;
            if (baseRow < mRow) {
                // copy from bottom to top row
                for (i = actualWindowSize - 1; i >= 0; i--) {
                    mLines[baseRow - i] = mLines[mRow - i];
                }
            } else {
                // copy from top to bottom row
                for (i = 0; i < actualWindowSize; i++) {
                    mLines[baseRow - i] = mLines[mRow - i];
                }
            }
            // clear rest of the rows
            for (i = 0; i <= baseRow - windowSize; i++) {
                mLines[i] = null;
            }
            for (i = baseRow + 1; i < mLines.length; i++) {
                mLines[i] = null;
            }
        }

        private CCLineBuilder getLineBuffer(int row) {
            if (mLines[row] == null) {
                mLines[row] = new CCLineBuilder(mBlankLine);
            }
            return mLines[row];
        }
    }

    /*
     * CCData parses the raw CC byte pair into displayable chars,
     * misc control codes, Mid-Row or Preamble Address Codes.
     */
    private static class CCData {
        private final byte mType;
        private final byte mData1;
        private final byte mData2;

        private static final String[] mCtrlCodeMap = {
            "RCL", "BS" , "AOF", "AON",
            "DER", "RU2", "RU3", "RU4",
            "FON", "RDC", "TR" , "RTD",
            "EDM", "CR" , "ENM", "EOC",
        };

        private static final String[] mSpecialCharMap = {
            "\u00AE",
            "\u00B0",
            "\u00BD",
            "\u00BF",
            "\u2122",
            "\u00A2",
            "\u00A3",
            "\u266A", // Eighth note
            "\u00E0",
            "\u00A0", // Transparent space
            "\u00E8",
            "\u00E2",
            "\u00EA",
            "\u00EE",
            "\u00F4",
            "\u00FB",
        };

        private static final String[] mSpanishCharMap = {
            // Spanish and misc chars
            "\u00C1", // A
            "\u00C9", // E
            "\u00D3", // I
            "\u00DA", // O
            "\u00DC", // U
            "\u00FC", // u
            "\u2018", // opening single quote
            "\u00A1", // inverted exclamation mark
            "*",
            "'",
            "\u2014", // em dash
            "\u00A9", // Copyright
            "\u2120", // Servicemark
            "\u2022", // round bullet
            "\u201C", // opening double quote
            "\u201D", // closing double quote
            // French
            "\u00C0",
            "\u00C2",
            "\u00C7",
            "\u00C8",
            "\u00CA",
            "\u00CB",
            "\u00EB",
            "\u00CE",
            "\u00CF",
            "\u00EF",
            "\u00D4",
            "\u00D9",
            "\u00F9",
            "\u00DB",
            "\u00AB",
            "\u00BB"
        };

        private static final String[] mProtugueseCharMap = {
            // Portuguese
            "\u00C3",
            "\u00E3",
            "\u00CD",
            "\u00CC",
            "\u00EC",
            "\u00D2",
            "\u00F2",
            "\u00D5",
            "\u00F5",
            "{",
            "}",
            "\\",
            "^",
            "_",
            "|",
            "~",
            // German and misc chars
            "\u00C4",
            "\u00E4",
            "\u00D6",
            "\u00F6",
            "\u00DF",
            "\u00A5",
            "\u00A4",
            "\u2502", // vertical bar
            "\u00C5",
            "\u00E5",
            "\u00D8",
            "\u00F8",
            "\u250C", // top-left corner
            "\u2510", // top-right corner
            "\u2514", // lower-left corner
            "\u2518", // lower-right corner
        };

        static CCData[] fromByteArray(byte[] data) {
            CCData[] ccData = new CCData[data.length / 3];

            for (int i = 0; i < ccData.length; i++) {
                ccData[i] = new CCData(
                        data[i * 3],
                        data[i * 3 + 1],
                        data[i * 3 + 2]);
            }

            return ccData;
        }

        CCData(byte type, byte data1, byte data2) {
            mType = type;
            mData1 = data1;
            mData2 = data2;
        }

        int getCtrlCode() {
            if ((mData1 == 0x14 || mData1 == 0x1c)
                    && mData2 >= 0x20 && mData2 <= 0x2f) {
                return mData2;
            }
            return INVALID;
        }

        StyleCode getMidRow() {
            // only support standard Mid-row codes, ignore
            // optional background/foreground mid-row codes
            if ((mData1 == 0x11 || mData1 == 0x19)
                    && mData2 >= 0x20 && mData2 <= 0x2f) {
                return StyleCode.fromByte(mData2);
            }
            return null;
        }

        PAC getPAC() {
            if ((mData1 & 0x70) == 0x10
                    && (mData2 & 0x40) == 0x40
                    && ((mData1 & 0x07) != 0 || (mData2 & 0x20) == 0)) {
                return PAC.fromBytes(mData1, mData2);
            }
            return null;
        }

        int getTabOffset() {
            if ((mData1 == 0x17 || mData1 == 0x1f)
                    && mData2 >= 0x21 && mData2 <= 0x23) {
                return mData2 & 0x3;
            }
            return 0;
        }

        boolean isDisplayableChar() {
            return isBasicChar() || isSpecialChar() || isExtendedChar();
        }

        String getDisplayText() {
            String str = getBasicChars();

            if (str == null) {
                str =  getSpecialChar();

                if (str == null) {
                    str = getExtendedChar();
                }
            }

            return str;
        }

        private String ctrlCodeToString(int ctrlCode) {
            return mCtrlCodeMap[ctrlCode - 0x20];
        }

        private boolean isBasicChar() {
            return mData1 >= 0x20 && mData1 <= 0x7f;
        }

        private boolean isSpecialChar() {
            return ((mData1 == 0x11 || mData1 == 0x19)
                    && mData2 >= 0x30 && mData2 <= 0x3f);
        }

        private boolean isExtendedChar() {
            return ((mData1 == 0x12 || mData1 == 0x1A
                    || mData1 == 0x13 || mData1 == 0x1B)
                    && mData2 >= 0x20 && mData2 <= 0x3f);
        }

        private char getBasicChar(byte data) {
            char c;
            // replace the non-ASCII ones
            switch (data) {
                case 0x2A: c = '\u00E1'; break;
                case 0x5C: c = '\u00E9'; break;
                case 0x5E: c = '\u00ED'; break;
                case 0x5F: c = '\u00F3'; break;
                case 0x60: c = '\u00FA'; break;
                case 0x7B: c = '\u00E7'; break;
                case 0x7C: c = '\u00F7'; break;
                case 0x7D: c = '\u00D1'; break;
                case 0x7E: c = '\u00F1'; break;
                case 0x7F: c = '\u2588'; break; // Full block
                default: c = (char) data; break;
            }
            return c;
        }

        private String getBasicChars() {
            if (mData1 >= 0x20 && mData1 <= 0x7f) {
                StringBuilder builder = new StringBuilder(2);
                builder.append(getBasicChar(mData1));
                if (mData2 >= 0x20 && mData2 <= 0x7f) {
                    builder.append(getBasicChar(mData2));
                }
                return builder.toString();
            }

            return null;
        }

        private String getSpecialChar() {
            if ((mData1 == 0x11 || mData1 == 0x19)
                    && mData2 >= 0x30 && mData2 <= 0x3f) {
                return mSpecialCharMap[mData2 - 0x30];
            }

            return null;
        }

        private String getExtendedChar() {
            if ((mData1 == 0x12 || mData1 == 0x1A)
                    && mData2 >= 0x20 && mData2 <= 0x3f){
                // 1 Spanish/French char
                return mSpanishCharMap[mData2 - 0x20];
            } else if ((mData1 == 0x13 || mData1 == 0x1B)
                    && mData2 >= 0x20 && mData2 <= 0x3f){
                // 1 Portuguese/German/Danish char
                return mProtugueseCharMap[mData2 - 0x20];
            }

            return null;
        }

        @Override
        public String toString() {
            String str;

            if (mData1 < 0x10 && mData2 < 0x10) {
                // Null Pad, ignore
                return String.format("[%d]Null: %02x %02x", mType, mData1, mData2);
            }

            int ctrlCode = getCtrlCode();
            if (ctrlCode != INVALID) {
                return String.format("[%d]%s", mType, ctrlCodeToString(ctrlCode));
            }

            int tabOffset = getTabOffset();
            if (tabOffset > 0) {
                return String.format("[%d]Tab%d", mType, tabOffset);
            }

            PAC pac = getPAC();
            if (pac != null) {
                return String.format("[%d]PAC: %s", mType, pac.toString());
            }

            StyleCode m = getMidRow();
            if (m != null) {
                return String.format("[%d]Mid-row: %s", mType, m.toString());
            }

            if (isDisplayableChar()) {
                return String.format("[%d]Displayable: %s (%02x %02x)",
                        mType, getDisplayText(), mData1, mData2);
            }

            return String.format("[%d]Invalid: %02x %02x", mType, mData1, mData2);
        }
    }
}

/**
 * Widget capable of rendering CEA-608 closed captions.
 */
class Cea608CCWidget extends ClosedCaptionWidget implements Cea608CCParser.DisplayListener {
    private static final Rect mTextBounds = new Rect();
    private static final String mDummyText = "1234567890123456789012345678901234";

    public Cea608CCWidget(Context context) {
        this(context, null);
    }

    public Cea608CCWidget(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Cea608CCWidget(Context context, AttributeSet attrs, int defStyle) {
        this(context, attrs, defStyle, 0);
    }

    public Cea608CCWidget(Context context, AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    public ClosedCaptionLayout createCaptionLayout(Context context) {
        return new CCLayout(context);
    }

    @Override
    public void onDisplayChanged(SpannableStringBuilder[] styledTexts) {
        ((CCLayout) mClosedCaptionLayout).update(styledTexts);

        if (mListener != null) {
            mListener.onChanged(this);
        }
    }

    @Override
    public CaptionStyle getCaptionStyle() {
        return mCaptionStyle;
    }

    private static class CCLineBox extends TextView {
        private static final float FONT_PADDING_RATIO = 0.75f;
        private static final float EDGE_OUTLINE_RATIO = 0.1f;
        private static final float EDGE_SHADOW_RATIO = 0.05f;
        private float mOutlineWidth;
        private float mShadowRadius;
        private float mShadowOffset;

        private int mTextColor = Color.WHITE;
        private int mBgColor = Color.BLACK;
        private int mEdgeType = CaptionStyle.EDGE_TYPE_NONE;
        private int mEdgeColor = Color.TRANSPARENT;

        CCLineBox(Context context) {
            super(context);
            setGravity(Gravity.CENTER);
            setBackgroundColor(Color.TRANSPARENT);
            setTextColor(Color.WHITE);
            setTypeface(Typeface.MONOSPACE);
            setVisibility(View.INVISIBLE);

            final Resources res = getContext().getResources();

            // get the default (will be updated later during measure)
            mOutlineWidth = res.getDimensionPixelSize(
                    com.android.internal.R.dimen.subtitle_outline_width);
            mShadowRadius = res.getDimensionPixelSize(
                    com.android.internal.R.dimen.subtitle_shadow_radius);
            mShadowOffset = res.getDimensionPixelSize(
                    com.android.internal.R.dimen.subtitle_shadow_offset);
        }

        void setCaptionStyle(CaptionStyle captionStyle) {
            mTextColor = captionStyle.foregroundColor;
            mBgColor = captionStyle.backgroundColor;
            mEdgeType = captionStyle.edgeType;
            mEdgeColor = captionStyle.edgeColor;

            setTextColor(mTextColor);
            if (mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
                setShadowLayer(mShadowRadius, mShadowOffset, mShadowOffset, mEdgeColor);
            } else {
                setShadowLayer(0, 0, 0, 0);
            }
            invalidate();
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            float fontSize = MeasureSpec.getSize(heightMeasureSpec) * FONT_PADDING_RATIO;
            setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSize);

            mOutlineWidth = EDGE_OUTLINE_RATIO * fontSize + 1.0f;
            mShadowRadius = EDGE_SHADOW_RATIO * fontSize + 1.0f;;
            mShadowOffset = mShadowRadius;

            // set font scale in the X direction to match the required width
            setScaleX(1.0f);
            getPaint().getTextBounds(mDummyText, 0, mDummyText.length(), mTextBounds);
            float actualTextWidth = mTextBounds.width();
            float requiredTextWidth = MeasureSpec.getSize(widthMeasureSpec);
            setScaleX(requiredTextWidth / actualTextWidth);

            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

        @Override
        protected void onDraw(Canvas c) {
            if (mEdgeType == CaptionStyle.EDGE_TYPE_UNSPECIFIED
                    || mEdgeType == CaptionStyle.EDGE_TYPE_NONE
                    || mEdgeType == CaptionStyle.EDGE_TYPE_DROP_SHADOW) {
                // these edge styles don't require a second pass
                super.onDraw(c);
                return;
            }

            if (mEdgeType == CaptionStyle.EDGE_TYPE_OUTLINE) {
                drawEdgeOutline(c);
            } else {
                // Raised or depressed
                drawEdgeRaisedOrDepressed(c);
            }
        }

        private void drawEdgeOutline(Canvas c) {
            TextPaint textPaint = getPaint();

            Paint.Style previousStyle = textPaint.getStyle();
            Paint.Join previousJoin = textPaint.getStrokeJoin();
            float previousWidth = textPaint.getStrokeWidth();

            setTextColor(mEdgeColor);
            textPaint.setStyle(Paint.Style.FILL_AND_STROKE);
            textPaint.setStrokeJoin(Paint.Join.ROUND);
            textPaint.setStrokeWidth(mOutlineWidth);

            // Draw outline and background only.
            super.onDraw(c);

            // Restore original settings.
            setTextColor(mTextColor);
            textPaint.setStyle(previousStyle);
            textPaint.setStrokeJoin(previousJoin);
            textPaint.setStrokeWidth(previousWidth);

            // Remove the background.
            setBackgroundSpans(Color.TRANSPARENT);
            // Draw foreground only.
            super.onDraw(c);
            // Restore the background.
            setBackgroundSpans(mBgColor);
        }

        private void drawEdgeRaisedOrDepressed(Canvas c) {
            TextPaint textPaint = getPaint();

            Paint.Style previousStyle = textPaint.getStyle();
            textPaint.setStyle(Paint.Style.FILL);

            final boolean raised = mEdgeType == CaptionStyle.EDGE_TYPE_RAISED;
            final int colorUp = raised ? Color.WHITE : mEdgeColor;
            final int colorDown = raised ? mEdgeColor : Color.WHITE;
            final float offset = mShadowRadius / 2f;

            // Draw background and text with shadow up
            setShadowLayer(mShadowRadius, -offset, -offset, colorUp);
            super.onDraw(c);

            // Remove the background.
            setBackgroundSpans(Color.TRANSPARENT);

            // Draw text with shadow down
            setShadowLayer(mShadowRadius, +offset, +offset, colorDown);
            super.onDraw(c);

            // Restore settings
            textPaint.setStyle(previousStyle);

            // Restore the background.
            setBackgroundSpans(mBgColor);
        }

        private void setBackgroundSpans(int color) {
            CharSequence text = getText();
            if (text instanceof Spannable) {
                Spannable spannable = (Spannable) text;
                Cea608CCParser.MutableBackgroundColorSpan[] bgSpans = spannable.getSpans(
                        0, spannable.length(), Cea608CCParser.MutableBackgroundColorSpan.class);
                for (int i = 0; i < bgSpans.length; i++) {
                    bgSpans[i].setBackgroundColor(color);
                }
            }
        }
    }

    private static class CCLayout extends LinearLayout implements ClosedCaptionLayout {
        private static final int MAX_ROWS = Cea608CCParser.MAX_ROWS;
        private static final float SAFE_AREA_RATIO = 0.9f;

        private final CCLineBox[] mLineBoxes = new CCLineBox[MAX_ROWS];

        CCLayout(Context context) {
            super(context);
            setGravity(Gravity.START);
            setOrientation(LinearLayout.VERTICAL);
            for (int i = 0; i < MAX_ROWS; i++) {
                mLineBoxes[i] = new CCLineBox(getContext());
                addView(mLineBoxes[i], LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            }
        }

        @Override
        public void setCaptionStyle(CaptionStyle captionStyle) {
            for (int i = 0; i < MAX_ROWS; i++) {
                mLineBoxes[i].setCaptionStyle(captionStyle);
            }
        }

        @Override
        public void setFontScale(float fontScale) {
            // Ignores the font scale changes of the system wide CC preference.
        }

        void update(SpannableStringBuilder[] textBuffer) {
            for (int i = 0; i < MAX_ROWS; i++) {
                if (textBuffer[i] != null) {
                    mLineBoxes[i].setText(textBuffer[i], TextView.BufferType.SPANNABLE);
                    mLineBoxes[i].setVisibility(View.VISIBLE);
                } else {
                    mLineBoxes[i].setVisibility(View.INVISIBLE);
                }
            }
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);

            int safeWidth = getMeasuredWidth();
            int safeHeight = getMeasuredHeight();

            // CEA-608 assumes 4:3 video
            if (safeWidth * 3 >= safeHeight * 4) {
                safeWidth = safeHeight * 4 / 3;
            } else {
                safeHeight = safeWidth * 3 / 4;
            }
            safeWidth *= SAFE_AREA_RATIO;
            safeHeight *= SAFE_AREA_RATIO;

            int lineHeight = safeHeight / MAX_ROWS;
            int lineHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                    lineHeight, MeasureSpec.EXACTLY);
            int lineWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                    safeWidth, MeasureSpec.EXACTLY);

            for (int i = 0; i < MAX_ROWS; i++) {
                mLineBoxes[i].measure(lineWidthMeasureSpec, lineHeightMeasureSpec);
            }
        }

        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            // safe caption area
            int viewPortWidth = r - l;
            int viewPortHeight = b - t;
            int safeWidth, safeHeight;
            // CEA-608 assumes 4:3 video
            if (viewPortWidth * 3 >= viewPortHeight * 4) {
                safeWidth = viewPortHeight * 4 / 3;
                safeHeight = viewPortHeight;
            } else {
                safeWidth = viewPortWidth;
                safeHeight = viewPortWidth * 3 / 4;
            }
            safeWidth *= SAFE_AREA_RATIO;
            safeHeight *= SAFE_AREA_RATIO;
            int left = (viewPortWidth - safeWidth) / 2;
            int top = (viewPortHeight - safeHeight) / 2;

            for (int i = 0; i < MAX_ROWS; i++) {
                mLineBoxes[i].layout(
                        left,
                        top + safeHeight * i / MAX_ROWS,
                        left + safeWidth,
                        top + safeHeight * (i + 1) / MAX_ROWS);
            }
        }
    }
}
