/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor;

import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.CharacterStyle;
import android.text.style.ParagraphStyle;

import java.lang.reflect.Array;

/**
 * Clones the Spannable part of an editor by copying the text, all
 * CharacterStyle, and all ParagraphStyle spans to a new Spannable object
 * (used for undo/redo).
 * <p>
 * The code is partly taken from the non-public class
 * android.text.SpannableStringInternal.
 */
public class ClonedSpannableString extends SpannableString {

    private Object[] mSpans;
    private int[] mSpanData;
    private int mSpanCount;

    private static final int START = 0;
    private static final int END = 1;
    private static final int FLAGS = 2;
    private static final int COLUMNS = 3;

    public ClonedSpannableString(Spanned source) {
        this((CharSequence) source);
    }

    public ClonedSpannableString(CharSequence source) {
        super(source.toString());    // the toString is important to prevent the super class from copying the spans
        init(source, 0, source.length());
    }

    private void init(CharSequence source, int start, int end) {
        int initial = 20;
        mSpans = new Object[initial];
        mSpanData = new int[initial * 3];

        if (source instanceof Spanned) {
            Spanned sp = (Spanned) source;
            for (Object span : sp.getSpans(start, end, Object.class)) {
                if (span instanceof CharacterStyle || span instanceof ParagraphStyle) {
                    int st = sp.getSpanStart(span);
                    int en = sp.getSpanEnd(span);
                    int fl = sp.getSpanFlags(span);

                    if (st < start) st = start;
                    if (en > end) en = end;

                    setSpan(span, st - start, en - start, fl);
                }
            }
        }
    }

    // ****************************************** SpannableString Methods *******************************************

    @Override
    public void setSpan(Object what, int start, int end, int flags) {
        if (mSpanCount + 1 >= mSpans.length) {
            int newsize = mSpanCount + 10;
            Object[] newtags = new Object[newsize];
            int[] newdata = new int[newsize * 3];

            System.arraycopy(mSpans, 0, newtags, 0, mSpanCount);
            System.arraycopy(mSpanData, 0, newdata, 0, mSpanCount * 3);

            mSpans = newtags;
            mSpanData = newdata;
        }

        mSpans[mSpanCount] = what;
        mSpanData[mSpanCount * COLUMNS + START] = start;
        mSpanData[mSpanCount * COLUMNS + END] = end;
        mSpanData[mSpanCount * COLUMNS + FLAGS] = flags;
        mSpanCount++;
    }

    @Override
    public void removeSpan(Object what) {
        int count = mSpanCount;
        Object[] spans = mSpans;
        int[] data = mSpanData;

        for (int i = count - 1; i >= 0; i--) {
            if (spans[i] == what) {
                int c = count - (i + 1);

                System.arraycopy(spans, i + 1, spans, i, c);
                System.arraycopy(data, (i + 1) * COLUMNS,
                        data, i * COLUMNS, c * COLUMNS);

                mSpanCount--;
                return;
            }
        }
    }

    public int getSpanStart(Object what) {
        int count = mSpanCount;
        Object[] spans = mSpans;
        int[] data = mSpanData;

        for (int i = count - 1; i >= 0; i--) {
            if (spans[i] == what) {
                return data[i * COLUMNS + START];
            }
        }

        return -1;
    }

    public int getSpanEnd(Object what) {
        int count = mSpanCount;
        Object[] spans = mSpans;
        int[] data = mSpanData;

        for (int i = count - 1; i >= 0; i--) {
            if (spans[i] == what) {
                return data[i * COLUMNS + END];
            }
        }

        return -1;
    }

    public int getSpanFlags(Object what) {
        int count = mSpanCount;
        Object[] spans = mSpans;
        int[] data = mSpanData;

        for (int i = count - 1; i >= 0; i--) {
            if (spans[i] == what) {
                return data[i * COLUMNS + FLAGS];
            }
        }

        return 0;
    }

    @SuppressWarnings("unchecked")
    public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
        int count = 0;

        int spanCount = mSpanCount;
        Object[] spans = mSpans;
        int[] data = mSpanData;
        Object[] ret = null;
        Object ret1 = null;

        for (int i = 0; i < spanCount; i++) {
            if (kind != null && !kind.isInstance(spans[i])) {
                continue;
            }

            int spanStart = data[i * COLUMNS + START];
            int spanEnd = data[i * COLUMNS + END];

            if (spanStart > queryEnd) {
                continue;
            }
            if (spanEnd < queryStart) {
                continue;
            }

            if (spanStart != spanEnd && queryStart != queryEnd) {
                if (spanStart == queryEnd) {
                    continue;
                }
                if (spanEnd == queryStart) {
                    continue;
                }
            }

            if (count == 0) {
                ret1 = spans[i];
                count++;
            } else {
                if (count == 1) {
                    ret = (Object[]) Array.newInstance(kind, spanCount - i + 1);
                    ret[0] = ret1;
                }

                int prio = data[i * COLUMNS + FLAGS] & Spanned.SPAN_PRIORITY;
                if (prio != 0) {
                    int j;

                    for (j = 0; j < count; j++) {
                        int p = getSpanFlags(ret[j]) & Spanned.SPAN_PRIORITY;

                        if (prio > p) {
                            break;
                        }
                    }

                    System.arraycopy(ret, j, ret, j + 1, count - j);
                    ret[j] = spans[i];
                    count++;
                } else {
                    ret[count++] = spans[i];
                }
            }
        }

        if (count == 0) {
            return (T[]) Array.newInstance(kind, 0);
        }
        if (count == 1) {
            ret = (Object[]) Array.newInstance(kind, 1);
            ret[0] = ret1;
            return (T[]) ret;
        }
        if (count == ret.length) {
            return (T[]) ret;
        }

        Object[] nret = (Object[]) Array.newInstance(kind, count);
        System.arraycopy(ret, 0, nret, 0, count);
        return (T[]) nret;
    }

    @SuppressWarnings("rawtypes")
    public int nextSpanTransition(int start, int limit, Class kind) {
        int count = mSpanCount;
        Object[] spans = mSpans;
        int[] data = mSpanData;

        if (kind == null) {
            kind = Object.class;
        }

        for (int i = 0; i < count; i++) {
            int st = data[i * COLUMNS + START];
            int en = data[i * COLUMNS + END];

            if (st > start && st < limit && kind.isInstance(spans[i]))
                limit = st;
            if (en > start && en < limit && kind.isInstance(spans[i]))
                limit = en;
        }

        return limit;
    }
}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.toolbar.spinner;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.util.SparseArray;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;

import com.onegravity.rteditor.toolbar.R;

import java.util.List;

/**
 * This class implements an ArrayAdapter using the parameters from the
 * SpinnerItems object. Use the setter method in SpinnerItems to change the
 * layout of the spinner item or the spinner entry items. Overwrite the
 * getDropDownView() method if you need a more complex layout than a simple
 * TextView for the spinner entry items.
 */
public class SpinnerItemAdapter<T extends SpinnerItem> extends BaseAdapter implements SpinnerItem.OnChangedListener {

    final private int mSpinnerId;
    final private int mSpinnerItemId;

    private int mSelectedItem;
    private List<T> mItems;

    private LayoutInflater mInflater;

    // we need this to update the spinner text, for some reason the layout view
    private ViewGroup mParent;
    private String mSpinnerTitle;

    private Handler mHandler;

    final private SparseArray<View> mViewCache = new SparseArray<>();

    private int mSelectedBackgroundId;

    public SpinnerItemAdapter(Context context, SpinnerItems<T> spinnerItems, int spinnerId, int spinnerItemId) {
        mSelectedItem = spinnerItems.getSelectedItem();
        mItems = spinnerItems.getItems();
        mInflater = LayoutInflater.from(context);
        mHandler = new Handler();
        mSpinnerId = spinnerId;
        mSpinnerItemId = spinnerItemId;

        TypedValue typedValue = new TypedValue();
        context.getTheme().resolveAttribute(R.attr.rte_ToolbarSpinnerSelectedColor, typedValue, true);
        mSelectedBackgroundId = typedValue.resourceId;
    }

    /**
     * This method returns the spinner view
     */
    @SuppressLint("ViewHolder")
    @Override
    public final View getView(int position, View convertView, ViewGroup parent) {
        View spinnerView = mInflater.inflate(mSpinnerId, parent, false);

        mParent = parent;
        TextView spinnerTitleView = (TextView) spinnerView.findViewById(R.id.title);
        updateSpinnerTitle(spinnerTitleView);
        return spinnerView;
    }

    /**
     * Returns the spinner entry view
     */
    @SuppressLint("InlinedApi")
    @Override
    public View getDropDownView(int position, View convertView, ViewGroup parent) {
        SpinnerItem spinnerItem = mItems.get(position);
        spinnerItem.setOnChangedListener(this, position);

        // we can't use the convertView because it keeps handing us spinner layouts (mSpinnerId)
        View spinnerItemView = mInflater.inflate(mSpinnerItemId, parent, false);
        int key = (position << 16) + getItemViewType(position);
        mViewCache.put(key, spinnerItemView);

        bindView(position, spinnerItemView, spinnerItem);

        return spinnerItemView;
    }

    @Override
    public void onSpinnerItemChanged(final Object tag) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                int position = (Integer) tag;
                int key = (position << 16) + getItemViewType(position);
                SpinnerItem spinnerItem = mItems.get(position);
                View spinnerItemView = mViewCache.get(key);
                if (spinnerItemView != null) {
                    bindView(position, spinnerItemView, spinnerItem);
                }
            }
        });
    }

    private void bindView(int position, View spinnerItemView, SpinnerItem spinnerItem) {
        // configure spinner name
        TextView nameView = (TextView) spinnerItemView.findViewById(R.id.spinner_name);
        spinnerItem.formatNameView(nameView);

        // configure spinner color
        View colorView = spinnerItemView.findViewById(R.id.spinner_color);
        spinnerItem.formatColorView(colorView);

        // set background for selected item
        View textContainer = spinnerItemView.findViewById(R.id.chip_pacemaker);
        textContainer = textContainer == null ? nameView : textContainer;
        textContainer.setBackgroundResource(position == mSelectedItem ? mSelectedBackgroundId : android.R.color.transparent);
    }

    /**
     * @param position position of the selected item (0..n-1) or -1 if no item is selected
     */
    public void setSelectedItem(int position) {
        mSelectedItem = position;
    }

    /**
     * @return position of the selected item (0..n-1) or -1 if no item is selected
     */
    public int getSelectedItem() {
        return mSelectedItem;
    }

    public void updateSpinnerTitle(String title) {
        mSpinnerTitle = title;
        if (mParent != null) {
            try {
                TextView spinnerTitleView = ((TextView) mParent.getChildAt(0).findViewById(R.id.title));
                updateSpinnerTitle(spinnerTitleView);
            } catch (Exception ignore) { /* this is a hack but better safe than sorry */ }
        }
    }

    private void updateSpinnerTitle(TextView titleView) {
        if (titleView != null) {
            titleView.setText(mSpinnerTitle);
            titleView.setVisibility((mSpinnerTitle == null) ? View.GONE : View.VISIBLE);
            titleView.setHorizontallyScrolling(true);
        }
    }

    @Override
    public int getCount() {
        return mItems.size();
    }

    @Override
    public T getItem(int position) {
        return mItems.get(position);
    }

    @Override
    public int getViewTypeCount() {
        return 1;
    }

    @Override
    public int getItemViewType(int position) {
        return 0;
    }

    @Override
    public boolean isEnabled(int position) {
        return true;
    }

    @Override
    public boolean areAllItemsEnabled() {
        return false;
    }

    @Override
    public boolean hasStableIds() {
        return false;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.converter;

import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.ParagraphStyle;
import android.text.style.StrikethroughSpan;
import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.URLSpan;
import com.onegravity.rteditor.spans.UnderlineSpan;

import com.onegravity.rteditor.api.format.RTFormat;
import com.onegravity.rteditor.api.format.RTHtml;
import com.onegravity.rteditor.api.media.RTAudio;
import com.onegravity.rteditor.api.media.RTImage;
import com.onegravity.rteditor.api.media.RTVideo;
import com.onegravity.rteditor.converter.tagsoup.util.StringEscapeUtils;
import com.onegravity.rteditor.spans.AudioSpan;
import com.onegravity.rteditor.spans.BoldSpan;
import com.onegravity.rteditor.spans.TypefaceSpan;
import com.onegravity.rteditor.spans.ImageSpan;
import com.onegravity.rteditor.spans.ItalicSpan;
import com.onegravity.rteditor.spans.LinkSpan;
import com.onegravity.rteditor.spans.VideoSpan;
import com.onegravity.rteditor.utils.Helper;
import com.onegravity.rteditor.utils.Paragraph;
import com.onegravity.rteditor.utils.RTLayout;
import com.onegravity.rteditor.utils.Selection;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.Stack;
import java.util.TreeSet;

/**
 * Converts Spanned text to html
 */
public class ConverterSpannedToHtml {

    private static final String BR = "
\n";
    private static final String LT = "<";
    private static final String GT = ">";
    private static final String AMP = "&";
    private static final String NBSP = " ";

    private StringBuilder mOut;
    private Spanned mText;
    private RTFormat mRTFormat;
    private List<RTImage> mImages;
    private Stack<AccumulatedParagraphStyle> mParagraphStyles = new Stack<AccumulatedParagraphStyle>();

    /**
     * Converts a spanned text to HTML
     */
    public RTHtml<RTImage, RTAudio, RTVideo> convert(final Spanned text, RTFormat.Html rtFormat) {
        mText = text;
        mRTFormat = rtFormat;

        mOut = new StringBuilder();
        mImages = new ArrayList<>();
        mParagraphStyles.clear();

        // convert paragraphs
        convertParagraphs();

        return new RTHtml<>(rtFormat, mOut.toString(), mImages);
    }

    // ****************************************** Process Paragraphs *******************************************

    private void convertParagraphs() {
        RTLayout rtLayout = new RTLayout(mText);

        // a manual for loop is faster than the for-each loop for an ArrayList:
        // see https://developer.android.com/training/articles/perf-tips.html#Loops
        ArrayList<Paragraph> paragraphs = rtLayout.getParagraphs();
        for (int i = 0, size = paragraphs.size(); i < size; i++) {
            Paragraph paragraph = paragraphs.get(i);

            // retrieve all spans for this paragraph
            Set<SingleParagraphStyle> styles = getParagraphStyles(mText, paragraph);

            // get the alignment span if there is any
            ParagraphType alignmentType = null;
            for (SingleParagraphStyle style : styles) {
                if (style.getType().isAlignment()) {
                    alignmentType = style.getType();
                    break;
                }
            }

            /*
             * start tag: bullet points, numbering and indentation
             */
            int newIndent = 0;
            ParagraphType newType = ParagraphType.NONE;
            for (SingleParagraphStyle style : styles) {
                newIndent += style.getIndentation();
                ParagraphType type = style.getType();
                newType = type.isBullet() ? ParagraphType.BULLET :
                        type.isNumbering() ? ParagraphType.NUMBERING :
                                type.isIndentation() && newType.isUndefined() ? ParagraphType.INDENTATION_UL : newType;
            }
            // process leading margin style
            processLeadingMarginStyle(new AccumulatedParagraphStyle(newType, newIndent, 0));
            // add start list tag
            mOut.append(newType.getListStartTag());

            /*
             * start tag: alignment (left, center, right)
             */
            if (alignmentType != null) {
                mOut.append(alignmentType.getStartTag());
            }

            /*
             * Convert the plain text
             */
            withinParagraph(mText, paragraph.start(), paragraph.end());

            /*
             * end tag: alignment (left, center, right)
             */
            if (alignmentType != null) {
                removeTrailingLineBreak(alignmentType);
                mOut.append(alignmentType.getEndTag());
            }

            // add end list tag
            removeTrailingLineBreak(newType);
            mOut.append(newType.getListEndTag());
        }

        /*
         * end tag: bullet points and indentation
         */
        while (!mParagraphStyles.isEmpty()) {
            removeParagraph();
        }
    }

    private void removeTrailingLineBreak(ParagraphType type) {
        if (type.endTagAddsLineBreak() && mOut.length() >= BR.length()) {
            int start = mOut.length() - BR.length();
            int end = mOut.length();
            if (mOut.subSequence(start, end).equals(BR)) {
                mOut.delete(start, end);
            }
        }
    }

    private Set<SingleParagraphStyle> getParagraphStyles(final Spanned text, Selection selection) {
        Set<SingleParagraphStyle> styles = new HashSet<SingleParagraphStyle>();

        for (ParagraphStyle style : text.getSpans(selection.start(), selection.end(), ParagraphStyle.class)) {
            ParagraphType type = ParagraphType.getInstance(style);
            if (type != null) {
                styles.add(new SingleParagraphStyle(type, style));
            }
        }

        return styles;
    }

    private void processLeadingMarginStyle(AccumulatedParagraphStyle newStyle) {
        int currentIndent = 0;
        ParagraphType currentType = ParagraphType.NONE;
        if (!mParagraphStyles.isEmpty()) {
            AccumulatedParagraphStyle currentStyle = mParagraphStyles.peek();
            currentIndent = currentStyle.getAbsoluteIndent();
            currentType = currentStyle.getType();
        }

        if (newStyle.getAbsoluteIndent() > currentIndent) {
            newStyle.setRelativeIndent(newStyle.getAbsoluteIndent() - currentIndent);
            addParagraph(newStyle);
        } else if (newStyle.getAbsoluteIndent() < currentIndent) {
            removeParagraph();
            processLeadingMarginStyle(newStyle);
        } else if (newStyle.getType() != currentType) {
            newStyle.setRelativeIndent(removeParagraph());
            addParagraph(newStyle);
        }
    }

    private int removeParagraph() {
        if (!mParagraphStyles.isEmpty()) {
            AccumulatedParagraphStyle style = mParagraphStyles.pop();
            String tag = style.getType().getEndTag();
            int indent = style.getRelativeIndent();
            for (int i = 0; i < indent; i++) {
                mOut.append(tag);
            }
            return style.getRelativeIndent();
        }
        return 0;
    }

    private void addParagraph(AccumulatedParagraphStyle style) {
        String tag = style.getType().getStartTag();
        int indent = style.getRelativeIndent();
        for (int i = 0; i < indent; i++) {
            mOut.append(tag);
        }
        mParagraphStyles.push(style);
    }

    // ****************************************** Process Text *******************************************

    /**
     * Convert a spanned text within a paragraph
     */
    private void withinParagraph(final Spanned text, int start, int end) {
        // create sorted set of CharacterStyles
        SortedSet<CharacterStyle> sortedSpans = new TreeSet<>((s1, s2) -> {
            int start1 = text.getSpanStart(s1);
            int start2 = text.getSpanStart(s2);
            if (start1 != start2)
                return start1 - start2;        // span which starts first comes first

            int end1 = text.getSpanEnd(s1);
            int end2 = text.getSpanEnd(s2);
            if (end1 != end2) return end2 - end1;                // longer span comes first

            // if the paragraphs have the same span [start, end] we compare their name
            // compare the name only because local + anonymous classes have no canonical name
            return s1.getClass().getName().compareTo(s2.getClass().getName());
        });
        List<CharacterStyle> spanList = Arrays.asList(text.getSpans(start, end, CharacterStyle.class));
        sortedSpans.addAll(spanList);

        // process paragraphs/divs
        convertText(text, start, end, sortedSpans);
    }

    private void convertText(Spanned text, int start, int end, SortedSet<CharacterStyle> spans) {
        while (start < end) {

            // get first CharacterStyle
            CharacterStyle span = spans.isEmpty() ? null : spans.first();
            int spanStart = span == null ? Integer.MAX_VALUE : text.getSpanStart(span);
            int spanEnd = span == null ? Integer.MAX_VALUE : text.getSpanEnd(span);

            if (start < spanStart) {

                // no paragraph, just plain text
                escape(text, start, Math.min(end, spanStart));
                start = spanStart;

            } else {

                // CharacterStyle found

                spans.remove(span);

                if (handleStartTag(span)) {
                    convertText(text, Math.max(spanStart, start), Math.min(spanEnd, end), spans);
                }
                handleEndTag(span);

                start = spanEnd;
            }

        }
    }

    /**
     * @return True if the text between the tags should be converted too, False if it should be skipped (ImageSpan e.g.)
     */
    private boolean handleStartTag(CharacterStyle style) {
        if (style instanceof BoldSpan) {
            mOut.append("");
        } else if (style instanceof ItalicSpan) {
            mOut.append("");
        } else if (style instanceof UnderlineSpan) {
            mOut.append("");
        } else if (style instanceof SuperscriptSpan) {
            mOut.append("");
        } else if (style instanceof SubscriptSpan) {
            mOut.append("");
        } else if (style instanceof StrikethroughSpan) {
            mOut.append("");
        }
        /* Examples for fonts styles:
           This is heading 1
           This is heading 2
        */
        else if (style instanceof TypefaceSpan) {
            mOut.append("");
        } else if (style instanceof AbsoluteSizeSpan) {
            mOut.append("");
        } else if (style instanceof ForegroundColorSpan) {
            mOut.append("<font style=\"color:#");
            String color = Integer.toHexString(((ForegroundColorSpan) style).getForegroundColor() + 0x01000000);
            while (color.length() < 6) {
                color = "0" + color;
            }
            mOut.append(color);
            mOut.append("\">");
        } else if (style instanceof BackgroundColorSpan) {
            mOut.append("<font style=\"background-color:#");
            String color = Integer.toHexString(((BackgroundColorSpan) style).getBackgroundColor() + 0x01000000);
            while (color.length() < 6) {
                color = "0" + color;
            }
            mOut.append(color);
            mOut.append("\">");
        } else if (style instanceof LinkSpan) {
            mOut.append("");
        } else if (style instanceof ImageSpan) {
            ImageSpan span = ((ImageSpan) style);
            RTImage image = span.getImage();
            mImages.add(image);
            String filePath = image.getFilePath(mRTFormat);
            mOut.append("");
            return false;    // don't output the dummy character underlying the image.
        } else if (style instanceof AudioSpan) {
            AudioSpan span = ((AudioSpan) style);
            RTAudio audio = span.getAudio();
            String filePath = audio.getFilePath(mRTFormat);
            mOut.append("<embed src=\"" + filePath + "\">");
            return false;    // don't output the dummy character underlying the audio file.
        } else if (style instanceof VideoSpan) {
            VideoSpan span = ((VideoSpan) style);
            RTVideo video = span.getVideo();
            String filePath = video.getFilePath(mRTFormat);
            mOut.append("<video controls src=\"" + filePath + "\">");
            return false;    // don't output the dummy character underlying the video.
        }
        return true;
    }

    private void handleEndTag(CharacterStyle style) {
        if (style instanceof URLSpan) {
            mOut.append("");
        } else if (style instanceof TypefaceSpan) {
            mOut.append("");
        } else if (style instanceof ForegroundColorSpan) {
            mOut.append("");
        } else if (style instanceof BackgroundColorSpan) {
            mOut.append("");
        } else if (style instanceof AbsoluteSizeSpan) {
            mOut.append("");
        } else if (style instanceof StrikethroughSpan) {
            mOut.append("");
        } else if (style instanceof SubscriptSpan) {
            mOut.append("");
        } else if (style instanceof SuperscriptSpan) {
            mOut.append("");
        } else if (style instanceof UnderlineSpan) {
            mOut.append("");
        } else if (style instanceof BoldSpan) {
            mOut.append("");
        } else if (style instanceof ItalicSpan) {
            mOut.append("");
        }
    }

    /**
     * Escape plain text parts: <, >, &, Space --> ^lt;, > etc.
     */
    private void escape(CharSequence text, int start, int end) {
        for (int i = start; i < end; i++) {
            char c = text.charAt(i);
            if (c == '\n') {
                mOut.append(BR);
            } else if (c == '<') {
                mOut.append(LT);
            } else if (c == '>') {
                mOut.append(GT);
            } else if (c == '&') {
                mOut.append(AMP);
            } else if (c == ' ') {
                while (i + 1 < end && text.charAt(i + 1) == ' ') {
                    mOut.append(NBSP);
                    i++;
                }
                mOut.append(' ');
            }
            // removed the c > 0x7E check to leave emoji unaltered
            else if (/*c > 0x7E || */c < ' ') {
                mOut.append("&#" + ((int) c) + ";");
            } else {
                mOut.append(c);
            }
        }
    }

}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.media;

import java.io.File;
import java.util.Calendar;

import com.onegravity.rteditor.utils.io.FilenameUtils;

import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore.MediaColumns;
import android.webkit.MimeTypeMap;

import com.onegravity.rteditor.utils.Helper;

/**
 * Collection of utility functions used in the media package
 */
public class MediaUtils {

    /**
     * Creates a file with a non-conflicting file name in a specified folder based on an existing file name.
     *
     * @param targetFolder The target folder (e.g. /sdcard/Android/data)
     * @param originalFile The source file including the path (e.g. /sdcard/image.jpg)
     * @param mimeType     If the originalFile has no extension (e.g. for files provided by picasa) we use the mime type
     *                     to determine the file extension
     * @return The non-conflicting file name in targetFolder (e.g. /sdcard/Android/data/158867324_201308071234568.jpg)
     */
    public static File createUniqueFile(File targetFolder, String originalFile, boolean keepOriginal) {
        String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(originalFile);
        return createUniqueFile(targetFolder, originalFile, mimeType, keepOriginal);
    }

    public static File createUniqueFile(File targetFolder, String originalFile, String mimeType, boolean keepOriginal) {
        /*
         * We try to get the extension from the file name first.
         * If that fails (e.g. for images provided by the picasa content provider)
         * we use the mime type to determine the extension.
         * The extension is important to be able to determine the correct content type
         * once we create a MIME message.
         */
        String extension = FilenameUtils.getExtension(originalFile);
        if (isNullOrEmpty(extension)) {
            extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
        }

        String random = Long.toString(Math.round(Math.random() * Integer.MAX_VALUE));        // random part
        long timestamp = Calendar.getInstance().getTimeInMillis();                            // time stamp

        if (keepOriginal) {
            String baseName = FilenameUtils.getBaseName(originalFile);
            return new File(targetFolder + File.separator + baseName + "_" + random + "_" + timestamp + "." + extension);
        } else {
            return new File(targetFolder + File.separator + random + "_" + timestamp + "." + extension);
        }
    }

    /**
     * Creates a file Uri for a file defined by its absolute path.
     * The method can handle the case of an absolute path (e.g. /data/data....)
     * and a Uri path containing the file:// scheme (e.g. file:///data/data...)
     */
    public static Uri createFileUri(String path) {
        if (path.startsWith("file://")) {
            return Uri.parse(path);
        }
        return Uri.fromFile(new File(path));
    }

    /**
     * Retrieve local file path for an arbitrary Uri
     *
     * @throws IllegalArgumentException If the uri is null or we can't resolve the uri to an absolute file path
     *                                  (meaning the uri isn't valid)
     */
    public static String determineOriginalFile(Context context, Uri uri) throws IllegalArgumentException {
        String originalFile = null;

        if (uri != null) {
            // Picasa on Android >= 3.0 or other files using content providers
            if (uri.getScheme().startsWith("content")) {
                originalFile = getPathFromUri(context, uri);
            }

            // Picasa on Android < 3.0
            if (uri.toString().matches("https?://\\w+\\.googleusercontent\\.com/.+")) {
                originalFile = uri.toString();
            }

            // local storage
            if (uri.getScheme().startsWith("file")) {
                originalFile = uri.toString().substring(7);
            }

            if (isNullOrEmpty(originalFile)) {
                throw new IllegalArgumentException("File path was null");
            }
        } else {
            throw new IllegalArgumentException("Image Uri was null!");
        }

        return originalFile;
    }

    private static String getPathFromUri(Context context, Uri imageUri) {
        String filePath = "";

        if (imageUri.toString().startsWith("content://com.android.gallery3d.provider")) {
            imageUri = Uri.parse(imageUri.toString().replace("com.android.gallery3d", "com.google.android.gallery3d"));
        }

        Cursor cursor = null;
        try {
            String column = MediaColumns.DATA;
            String[] proj = {MediaColumns.DATA};

            cursor = context.getContentResolver().query(imageUri, proj, null, null, null);
            cursor.moveToFirst();

            if (imageUri.toString().startsWith("content://com.google.android.gallery3d")) {
                filePath = imageUri.toString();
            } else {
                filePath = cursor.getString(cursor.getColumnIndexOrThrow(column));
            }
        } catch (Exception ignore) {
            // Google Drive content provider throws an exception that we ignore
            // content://com.google.android.apps.docs.storage
        } finally {
            Helper.closeQuietly(cursor);
        }

        if (isNullOrEmpty(filePath) || !new File(filePath).exists() ||
                imageUri.toString().startsWith("content://com.google.android.gallery3d")) {
            filePath = imageUri.toString();
        }

        return filePath;
    }

    private static boolean isNullOrEmpty(String string) {
        return string == null || string.length() == 0;
    }
}


// This file is part of TagSoup and is Copyright 2002-2008 by John Cowan.
//
// TagSoup is licensed under the Apache License,
// Version 2.0.  You may obtain a copy of this license at
// http://www.apache.org/licenses/LICENSE-2.0 .  You may also have
// additional legal rights not granted by this license.
//
// TagSoup is distributed in the hope that it will be useful, but
// unless required by applicable law or agreed to in writing, TagSoup
// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
// OF ANY KIND, either express or implied; not even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

package com.onegravity.rteditor.converter.tagsoup;

/**
 * This class represents an element type in the schema. An element type has a
 * name, a content model vector, a member-of vector, a flags vector, default
 * attributes, and a schema to which it belongs.
 *
 * @see Schema
 */

public class ElementType {

    private String theName; // element type name (Qname)
    private String theNamespace; // element type namespace name
    private String theLocalName; // element type local name
    private int theModel; // bitmap: what the element contains
    private int theMemberOf; // bitmap: what element is contained in
    private int theFlags; // bitmap: element flags
    private AttributesImpl theAtts; // default attributes
    private ElementType theParent; // parent of this element type
    private Schema theSchema; // schema to which this belongs

    /**
     * Construct an ElementType: but it's better to use Schema.element()
     * instead. The content model, member-of, and flags vectors are specified as
     * ints.
     *
     * @param name     The element type name
     * @param model    ORed-together bits representing the content models allowed in
     *                 the content of this element type
     * @param memberOf ORed-together bits representing the content models to which
     *                 this element type belongs
     * @param flags    ORed-together bits representing the flags associated with this
     *                 element type
     * @param schema   The schema with which this element type will be associated
     */

    public ElementType(String name, int model, int memberOf, int flags,
                       Schema schema) {
        theName = name;
        theModel = model;
        theMemberOf = memberOf;
        theFlags = flags;
        theAtts = new AttributesImpl();
        theSchema = schema;
        theNamespace = namespace(name, false);
        theLocalName = localName(name);
    }

    /**
     * Return a namespace name from a Qname. The attribute flag tells us whether
     * to return an empty namespace name if there is no prefix, or use the
     * schema default instead.
     *
     * @param name      The Qname
     * @param attribute True if name is an attribute name
     * @return The namespace name
     */
    public String namespace(String name, boolean attribute) {
        int colon = name.indexOf(':');
        if (colon == -1) {
            return attribute ? "" : theSchema.getURI();
        }
        String prefix = name.substring(0, colon);
        if (prefix.equals("xml")) {
            return "http://www.w3.org/XML/1998/namespace";
        } else {
            return ("urn:x-prefix:" + prefix).intern();
        }
    }

    /**
     * Return a local name from a Qname.
     *
     * @param name The Qname
     * @return The local name
     */
    public String localName(String name) {
        int colon = name.indexOf(':');
        if (colon == -1) {
            return name;
        } else {
            return name.substring(colon + 1).intern();
        }
    }

    /**
     * Returns the name of this element type.
     *
     * @return The name of the element type
     */

    public String name() {
        return theName;
    }

    /**
     * Returns the namespace name of this element type.
     *
     * @return The namespace name of the element type
     */

    public String namespace() {
        return theNamespace;
    }

    /**
     * Returns the local name of this element type.
     *
     * @return The local name of the element type
     */

    public String localName() {
        return theLocalName;
    }

    /**
     * Returns the content models of this element type.
     *
     * @return The content models of this element type as a vector of bits
     */

    public int model() {
        return theModel;
    }

    /**
     * Returns the content models to which this element type belongs.
     *
     * @return The content models to which this element type belongs as a vector
     * of bits
     */

    public int memberOf() {
        return theMemberOf;
    }

    /**
     * Returns the flags associated with this element type.
     *
     * @return The flags associated with this element type as a vector of bits
     */

    public int flags() {
        return theFlags;
    }

    /**
     * Returns the default attributes associated with this element type.
     * Attributes of type CDATA that don't have default values are typically not
     * included. Other attributes without default values have an internal value
     * of <tt>null</tt>. The return value is an AttributesImpl to allow the
     * caller to mutate the attributes.
     */

    public AttributesImpl atts() {
        return theAtts;
    }

    /**
     * Returns the parent element type of this element type.
     *
     * @return The parent element type
     */

    public ElementType parent() {
        return theParent;
    }

    /**
     * Returns the schema which this element type is associated with.
     *
     * @return The schema
     */

    public Schema schema() {
        return theSchema;
    }

    /**
     * Returns true if this element type can contain another element type. That
     * is, if any of the models in this element's model vector match any of the
     * models in the other element type's member-of vector.
     *
     * @param other The other element type
     */

    public boolean canContain(ElementType other) {
        return (theModel & other.theMemberOf) != 0;
    }

    /**
     * Sets an attribute and its value into an AttributesImpl object. Attempts
     * to set a namespace declaration are ignored.
     *
     * @param atts  The AttributesImpl object
     * @param name  The name (Qname) of the attribute
     * @param type  The type of the attribute
     * @param value The value of the attribute
     */

    public void setAttribute(AttributesImpl atts, String name, String type,
                             String value) {
        if (name.equals("xmlns") || name.startsWith("xmlns:")) {
            return;
        }
        ;
        String namespace = namespace(name, true);
        String localName = localName(name);
        int i = atts.getIndex(name);
        if (i == -1) {
            name = name.intern();
            if (type == null)
                type = "CDATA";
            if (!type.equals("CDATA"))
                value = normalize(value);
            atts.addAttribute(namespace, localName, name, type, value);
        } else {
            if (type == null)
                type = atts.getType(i);
            if (!type.equals("CDATA"))
                value = normalize(value);
            atts.setAttribute(i, namespace, localName, name, type, value);
        }
    }

    /**
     * Normalize an attribute value (ID-style). CDATA-style attribute
     * normalization is already done.
     *
     * @param value The value to normalize
     * @return The normalized value
     */
    public static String normalize(String value) {
        if (value == null)
            return value;
        value = value.trim();
        if (value.indexOf("  ") == -1)
            return value;
        boolean space = false;
        int len = value.length();
        StringBuffer b = new StringBuffer(len);
        for (int i = 0; i < len; i++) {
            char v = value.charAt(i);
            if (v == ' ') {
                if (!space)
                    b.append(v);
                space = true;
            } else {
                b.append(v);
                space = false;
            }
        }
        return b.toString();
    }

    /**
     * Sets an attribute and its value into this element type.
     *
     * @param name  The name of the attribute
     * @param type  The type of the attribute
     * @param value The value of the attribute
     */

    public void setAttribute(String name, String type, String value) {
        setAttribute(theAtts, name, type, value);
    }

    /**
     * Sets the models of this element type.
     *
     * @param model The content models of this element type as a vector of bits
     */

    public void setModel(int model) {
        theModel = model;
    }

    /**
     * Sets the content models to which this element type belongs.
     *
     * @param memberOf The content models to which this element type belongs as a
     *                 vector of bits
     */

    public void setMemberOf(int memberOf) {
        theMemberOf = memberOf;
    }

    /**
     * Sets the flags of this element type.
     *
     * @param flags associated with this element type The flags as a vector of
     *              bits
     */

    public void setFlags(int flags) {
        theFlags = flags;
    }

    /**
     * Sets the parent element type of this element type.
     *
     * @param parent The parent element type
     */

    public void setParent(ElementType parent) {
        theParent = parent;
    }

}


/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.onegravity.rteditor.utils.io.output;

import java.io.Serializable;
import java.io.Writer;

/**
 * {@link Writer} implementation that outputs to a {@link StringBuilder}.
 *

 * NOTE: This implementation, as an alternative to
 * java.io.StringWriter, provides an un-synchronized
 * (i.e. for use in a single thread) implementation for better performance.
 * For safe usage with multiple {@link Thread}s then
 * java.io.StringWriter should be used.
 *
 * @version $Id: StringBuilderWriter.java 1304052 2012-03-22 20:55:29Z ggregory $
 * @since 2.0
 */
public class StringBuilderWriter extends Writer implements Serializable {

    private final StringBuilder builder;

    /**
     * Construct a new {@link StringBuilder} instance with default capacity.
     */
    public StringBuilderWriter() {
        this.builder = new StringBuilder();
    }

    /**
     * Construct a new {@link StringBuilder} instance with the specified capacity.
     *
     * @param capacity The initial capacity of the underlying {@link StringBuilder}
     */
    public StringBuilderWriter(int capacity) {
        this.builder = new StringBuilder(capacity);
    }

    /**
     * Construct a new instance with the specified {@link StringBuilder}.
     *
     * @param builder The String builder
     */
    public StringBuilderWriter(StringBuilder builder) {
        this.builder = builder != null ? builder : new StringBuilder();
    }

    /**
     * Append a single character to this Writer.
     *
     * @param value The character to append
     * @return This writer instance
     */
    @Override
    public Writer append(char value) {
        builder.append(value);
        return this;
    }

    /**
     * Append a character sequence to this Writer.
     *
     * @param value The character to append
     * @return This writer instance
     */
    @Override
    public Writer append(CharSequence value) {
        builder.append(value);
        return this;
    }

    /**
     * Append a portion of a character sequence to the {@link StringBuilder}.
     *
     * @param value The character to append
     * @param start The index of the first character
     * @param end The index of the last character + 1
     * @return This writer instance
     */
    @Override
    public Writer append(CharSequence value, int start, int end) {
        builder.append(value, start, end);
        return this;
    }

    /**
     * Closing this writer has no effect.
     */
    @Override
    public void close() {
    }

    /**
     * Flushing this writer has no effect.
     */
    @Override
    public void flush() {
    }

    /**
     * Write a String to the {@link StringBuilder}.
     *
     * @param value The value to write
     */
    @Override
    public void write(String value) {
        if (value != null) {
            builder.append(value);
        }
    }

    /**
     * Write a portion of a character array to the {@link StringBuilder}.
     *
     * @param value The value to write
     * @param offset The index of the first character
     * @param length The number of characters to write
     */
    @Override
    public void write(char[] value, int offset, int length) {
        if (value != null) {
            builder.append(value, offset, length);
        }
    }

    /**
     * Return the underlying builder.
     *
     * @return The underlying builder
     */
    public StringBuilder getBuilder() {
        return builder;
    }

    /**
     * Returns {@link StringBuilder#toString()}.
     *
     * @return The contents of the String builder.
     */
    @Override
    public String toString() {
        return builder.toString();
    }
}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.fonts;

import android.content.res.AssetManager;

import com.onegravity.rteditor.utils.io.IOUtils;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;

/**
 * This class offers methods to analyze ttf files, namely to retrieve the font name.
 */
abstract class TTFAnalyzer {

    /**
     * Retrieve the file name for a system font.
     *
     * @param filePath the full path for the font to retrieve.
     *
     * @return The file name or null of none could be retrieved.
     */
    static String getFontName(String filePath) {
        TTFRandomAccessFile in = null;
        try {
            RandomAccessFile file = new RandomAccessFile(filePath, "r");
            in = new TTFRandomAccessFile(file);
            return getTTFFontName(in, filePath);
        } catch (IOException e) {
            return null;    // Missing permissions or corrupted font file?
        }
        finally {
            IOUtils.closeQuietly(in);
        }
    }

    /**
     * Retrieve the file name for a font in the asset folder.
     *
     * @param filePath the full path for the font in the asset folder to retrieve.
     *
     * @return The file name or null of none could be retrieved.
     */
    static String getFontName(AssetManager assets, String filePath) {
        TTFAssetInputStream in = null;
        try {
            InputStream file = assets.open(filePath, AssetManager.ACCESS_RANDOM);
            in = new TTFAssetInputStream(file);
            return getTTFFontName(in, filePath);
        } catch (FileNotFoundException e) {
            return null;    // Missing permissions?
        } catch (IOException e) {
            return null;    // Corrupted font file?
        }
        finally {
            IOUtils.closeQuietly(in);
        }
    }

    private static String getTTFFontName(TTFInputStream in, String fontFilename) {
        try {
            // Read the version first
            int version = readDword(in);

            // The version must be either 'true' (0x74727565) or 0x00010000 or 'OTTO' (0x4f54544f) for CFF style fonts.
            if (version != 0x74727565 && version != 0x00010000 && version != 0x4f54544f) {
                return null;
            }

            // The TTF file consist of several sections called "tables", and
            // we need to know how many of them are there.
            int numTables = readWord(in);

            // Skip the rest in the header
            readWord(in); // skip searchRange
            readWord(in); // skip entrySelector
            readWord(in); // skip rangeShift

            // Now we can read the tables
            for (int i = 0; i < numTables; i++) {
                // Read the table entry
                int tag = readDword(in);
                readDword(in); // skip checksum
                int offset = readDword(in);
                int length = readDword(in);

                // Now here' the trick. 'name' field actually contains the
                // textual string name.
                // So the 'name' string in characters equals to 0x6E616D65
                if (tag == 0x6E616D65) {
                    // Here's the name section. Read it completely into the
                    // allocated buffer
                    byte[] table = new byte[length];

                    in.seek(offset);
                    read(in, table);

                    // This is also a table. See
                    // http://developer.apple.com/fonts/ttrefman/rm06/Chap6name.html
                    // According to Table 36, the total number of table
                    // records is stored in the second word, at the offset
                    // 2.
                    // Getting the count and string offset - remembering
                    // it's big endian.
                    int count = getWord(table, 2);
                    int string_offset = getWord(table, 4);

                    // Record starts from offset 6
                    for (int record = 0; record < count; record++) {
                        // Table 37 tells us that each record is 6 words ->
                        // 12 bytes, and that the nameID is 4th word so its
                        // offset is 6.
                        // We also need to account for the first 6 bytes of
                        // the header above (Table 36), so...
                        int nameid_offset = record * 12 + 6;
                        int platformID = getWord(table, nameid_offset);
                        int nameid_value = getWord(table, nameid_offset + 6);

                        // Table 42 lists the valid name Identifiers. We're
                        // interested in 4 but not in Unicode encoding (for
                        // simplicity).
                        // The encoding is stored as PlatformID and we're
                        // interested in Mac encoding
                        if (nameid_value == 4 && platformID == 1) {
                            // We need the string offset and length, which
                            // are the word 6 and 5 respectively
                            int name_length = getWord(table,
                                    nameid_offset + 8);
                            int name_offset = getWord(table,
                                    nameid_offset + 10);

                            // The real name string offset is calculated by
                            // adding the string_offset
                            name_offset = name_offset + string_offset;

                            // Make sure it is inside the array
                            if (name_offset >= 0 && name_offset + name_length < table.length) {
                                return new String(table, name_offset, name_length);
                            }
                        }
                    }
                }
            }

            return null;
        } catch (FileNotFoundException e) {
            // Permissions?
            return null;
        } catch (IOException e) {
            // Most likely a corrupted font file
            return null;
        }
    }

    private static int readByte(TTFInputStream in) throws IOException {
        return in.read() & 0xFF;
    }

    private static int readWord(TTFInputStream in) throws IOException {
        int b1 = readByte(in);
        int b2 = readByte(in);

        return b1 << 8 | b2;
    }

    private static int readDword(TTFInputStream in) throws IOException {
        int b1 = readByte(in);
        int b2 = readByte(in);
        int b3 = readByte(in);
        int b4 = readByte(in);

        return b1 << 24 | b2 << 16 | b3 << 8 | b4;
    }

    private static void read(TTFInputStream in, byte[] array) throws IOException {
        if (in.read(array) != array.length) throw new IOException();
    }

    private static int getWord(byte[] array, int offset) {
        int b1 = array[offset] & 0xFF;
        int b2 = array[offset + 1] & 0xFF;

        return b1 << 8 | b2;
    }

}


/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.onegravity.rteditor.utils.io.output;

import com.onegravity.rteditor.utils.io.input.ClosedInputStream;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.SequenceInputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * This class implements an output stream in which the data is
 * written into a byte array. The buffer automatically grows as data
 * is written to it.
 *

 * The data can be retrieved using toByteArray() and
 * toString().
 *

 * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in
 * this class can be called after the stream has been closed without
 * generating an <tt>IOException</tt>.
 *

 * This is an alternative implementation of the {@link java.io.ByteArrayOutputStream}
 * class. The original implementation only allocates 32 bytes at the beginning.
 * As this class is designed for heavy duty it starts at 1024 bytes. In contrast
 * to the original it doesn't reallocate the whole memory block but allocates
 * additional buffers. This way no buffers need to be garbage collected and
 * the contents don't have to be copied to the new buffer. This class is
 * designed to behave exactly like the original. The only exception is the
 * deprecated toString(int) method that has been ignored.
 *
 * @version $Id: ByteArrayOutputStream.java 1304052 2012-03-22 20:55:29Z ggregory $
 */
public class ByteArrayOutputStream extends OutputStream {

    /** A singleton empty byte array. */
    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];

    /** The list of buffers, which grows and never reduces. */
    private final List<byte[]> buffers = new ArrayList<byte[]>();
    /** The index of the current buffer. */
    private int currentBufferIndex;
    /** The total count of bytes in all the filled buffers. */
    private int filledBufferSum;
    /** The current buffer. */
    private byte[] currentBuffer;
    /** The total count of bytes written. */
    private int count;

    /**
     * Creates a new byte array output stream. The buffer capacity is
     * initially 1024 bytes, though its size increases if necessary.
     */
    public ByteArrayOutputStream() {
        this(1024);
    }

    /**
     * Creates a new byte array output stream, with a buffer capacity of
     * the specified size, in bytes.
     *
     * @param size  the initial size
     * @throws IllegalArgumentException if size is negative
     */
    public ByteArrayOutputStream(int size) {
        if (size < 0) {
            throw new IllegalArgumentException(
                "Negative initial size: " + size);
        }
        synchronized (this) {
            needNewBuffer(size);
        }
    }

    /**
     * Makes a new buffer available either by allocating
     * a new one or re-cycling an existing one.
     *
     * @param newcount  the size of the buffer if one is created
     */
    private void needNewBuffer(int newcount) {
        if (currentBufferIndex < buffers.size() - 1) {
            //Recycling old buffer
            filledBufferSum += currentBuffer.length;

            currentBufferIndex++;
            currentBuffer = buffers.get(currentBufferIndex);
        } else {
            //Creating new buffer
            int newBufferSize;
            if (currentBuffer == null) {
                newBufferSize = newcount;
                filledBufferSum = 0;
            } else {
                newBufferSize = Math.max(
                    currentBuffer.length << 1,
                    newcount - filledBufferSum);
                filledBufferSum += currentBuffer.length;
            }

            currentBufferIndex++;
            currentBuffer = new byte[newBufferSize];
            buffers.add(currentBuffer);
        }
    }

    /**
     * Write the bytes to byte array.
     * @param b the bytes to write
     * @param off The start offset
     * @param len The number of bytes to write
     */
    @Override
    public void write(byte[] b, int off, int len) {
        if ((off < 0)
                || (off > b.length)
                || (len < 0)
                || ((off + len) > b.length)
                || ((off + len) < 0)) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return;
        }
        synchronized (this) {
            int newcount = count + len;
            int remaining = len;
            int inBufferPos = count - filledBufferSum;
            while (remaining > 0) {
                int part = Math.min(remaining, currentBuffer.length - inBufferPos);
                System.arraycopy(b, off + len - remaining, currentBuffer, inBufferPos, part);
                remaining -= part;
                if (remaining > 0) {
                    needNewBuffer(newcount);
                    inBufferPos = 0;
                }
            }
            count = newcount;
        }
    }

    /**
     * Write a byte to byte array.
     * @param b the byte to write
     */
    @Override
    public synchronized void write(int b) {
        int inBufferPos = count - filledBufferSum;
        if (inBufferPos == currentBuffer.length) {
            needNewBuffer(count + 1);
            inBufferPos = 0;
        }
        currentBuffer[inBufferPos] = (byte) b;
        count++;
    }

    /**
     * Writes the entire contents of the specified input stream to this
     * byte stream. Bytes from the input stream are read directly into the
     * internal buffers of this streams.
     *
     * @param in the input stream to read from
     * @return total number of bytes read from the input stream
     *         (and written to this stream)
     * @throws IOException if an I/O error occurs while reading the input stream
     * @since 1.4
     */
    public synchronized int write(InputStream in) throws IOException {
        int readCount = 0;
        int inBufferPos = count - filledBufferSum;
        int n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
        while (n != -1) {
            readCount += n;
            inBufferPos += n;
            count += n;
            if (inBufferPos == currentBuffer.length) {
                needNewBuffer(currentBuffer.length);
                inBufferPos = 0;
            }
            n = in.read(currentBuffer, inBufferPos, currentBuffer.length - inBufferPos);
        }
        return readCount;
    }

    /**
     * Return the current size of the byte array.
     * @return the current size of the byte array
     */
    public synchronized int size() {
        return count;
    }

    /**
     * Closing a <tt>ByteArrayOutputStream</tt> has no effect. The methods in
     * this class can be called after the stream has been closed without
     * generating an <tt>IOException</tt>.
     *
     * @throws IOException never (this method should not declare this exception
     * but it has to now due to backwards compatability)
     */
    @Override
    public void close() throws IOException {
        //nop
    }

    /**
     * @see java.io.ByteArrayOutputStream#reset()
     */
    public synchronized void reset() {
        count = 0;
        filledBufferSum = 0;
        currentBufferIndex = 0;
        currentBuffer = buffers.get(currentBufferIndex);
    }

    /**
     * Writes the entire contents of this byte stream to the
     * specified output stream.
     *
     * @param out  the output stream to write to
     * @throws IOException if an I/O error occurs, such as if the stream is closed
     * @see java.io.ByteArrayOutputStream#writeTo(OutputStream)
     */
    public synchronized void writeTo(OutputStream out) throws IOException {
        int remaining = count;
        for (byte[] buf : buffers) {
            int c = Math.min(buf.length, remaining);
            out.write(buf, 0, c);
            remaining -= c;
            if (remaining == 0) {
                break;
            }
        }
    }

    /**
     * Fetches entire contents of an InputStream and represent
     * same data as result InputStream.
     *

     * This method is useful where,
     *

     *
Source InputStream is slow.

     *
It has network resources associated, so we cannot keep it open for
     * long time.

     *
It has network timeout associated.

     *

     * It can be used in favor of {@link #toByteArray()}, since it
     * avoids unnecessary allocation and copy of byte[].

     * This method buffers the input internally, so there is no need to use a
     * BufferedInputStream.
     *
     * @param input Stream to be fully buffered.
     * @return A fully buffered stream.
     * @throws IOException if an I/O error occurs
     * @since 2.0
     */
    public static InputStream toBufferedInputStream(InputStream input)
            throws IOException {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        output.write(input);
        return output.toBufferedInputStream();
    }

    /**
     * Gets the current contents of this byte stream as a Input Stream. The
     * returned stream is backed by buffers of this stream,
     * avoiding memory allocation and copy, thus saving space and time.

     *
     * @return the current contents of this output stream.
     * @see java.io.ByteArrayOutputStream#toByteArray()
     * @see #reset()
     * @since 2.0
     */
    private InputStream toBufferedInputStream() {
        int remaining = count;
        if (remaining == 0) {
            return new ClosedInputStream();
        }
        List<ByteArrayInputStream> list = new ArrayList<ByteArrayInputStream>(buffers.size());
        for (byte[] buf : buffers) {
            int c = Math.min(buf.length, remaining);
            list.add(new ByteArrayInputStream(buf, 0, c));
            remaining -= c;
            if (remaining == 0) {
                break;
            }
        }
        return new SequenceInputStream(Collections.enumeration(list));
    }

    /**
     * Gets the curent contents of this byte stream as a byte array.
     * The result is independent of this stream.
     *
     * @return the current contents of this output stream, as a byte array
     * @see java.io.ByteArrayOutputStream#toByteArray()
     */
    public synchronized byte[] toByteArray() {
        int remaining = count;
        if (remaining == 0) {
            return EMPTY_BYTE_ARRAY;
        }
        byte newbuf[] = new byte[remaining];
        int pos = 0;
        for (byte[] buf : buffers) {
            int c = Math.min(buf.length, remaining);
            System.arraycopy(buf, 0, newbuf, pos, c);
            pos += c;
            remaining -= c;
            if (remaining == 0) {
                break;
            }
        }
        return newbuf;
    }

    /**
     * Gets the curent contents of this byte stream as a string.
     * @return the contents of the byte array as a String
     * @see java.io.ByteArrayOutputStream#toString()
     */
    @Override
    public String toString() {
        return new String(toByteArray());
    }

    /**
     * Gets the curent contents of this byte stream as a string
     * using the specified encoding.
     *
     * @param enc  the name of the character encoding
     * @return the string converted from the byte array
     * @throws UnsupportedEncodingException if the encoding is not supported
     * @see java.io.ByteArrayOutputStream#toString(String)
     */
    public String toString(String enc) throws UnsupportedEncodingException {
        return new String(toByteArray(), enc);
    }

}


/*
 * Copyright (C) 2015-2018 Emanuel Moecklin
 *
 * 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.onegravity.rteditor.toolbar;

import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.text.Layout;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.LinearLayout;
import android.widget.Spinner;

import com.onegravity.colorpicker.ColorPickerDialog;
import com.onegravity.colorpicker.ColorPickerListener;
import com.onegravity.colorpicker.SetColorPickerListenerEvent;
import com.onegravity.rteditor.RTToolbar;
import com.onegravity.rteditor.RTToolbarListener;
import com.onegravity.rteditor.effects.Effects;
import com.onegravity.rteditor.fonts.FontManager;
import com.onegravity.rteditor.fonts.RTTypeface;
import com.onegravity.rteditor.toolbar.spinner.BGColorSpinnerItem;
import com.onegravity.rteditor.toolbar.spinner.ColorSpinnerItem;
import com.onegravity.rteditor.toolbar.spinner.FontColorSpinnerItem;
import com.onegravity.rteditor.toolbar.spinner.FontSizeSpinnerItem;
import com.onegravity.rteditor.toolbar.spinner.FontSpinnerItem;
import com.onegravity.rteditor.toolbar.spinner.SpinnerItem;
import com.onegravity.rteditor.toolbar.spinner.SpinnerItemAdapter;
import com.onegravity.rteditor.toolbar.spinner.SpinnerItems;
import com.onegravity.rteditor.utils.Helper;

import java.util.SortedSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * This class is a concrete implementation of the RTToolbar interface. It uses
 * toggle buttons for the effects with a simple on/off (like bold/not bold) and
 * Spinners for the more complex formatting (background color, font color, font
 * size).
 * <p>
 * While the included rte_toolar layout puts all icons in a row it's easy to use
 * multiple toolbars, each with a subset of formatting options (e.g. one for the
 * character formatting, one for the paragraph formatting, one for all the rest
 * like insert image, undo/redo etc.).
 */
public class HorizontalRTToolbar extends LinearLayout implements RTToolbar, View.OnClickListener {

    /*
     * We need a unique id for the toolbar because the RTManager is capable of managing multiple toolbars
     */
    private static AtomicInteger sIdCounter = new AtomicInteger(0);
    private int mId;

    private RTToolbarListener mListener;

    private ViewGroup mToolbarContainer;

    /*
     * The buttons
     */
    private RTToolbarImageButton mBold;
    private RTToolbarImageButton mItalic;
    private RTToolbarImageButton mUnderline;
    private RTToolbarImageButton mStrikethrough;
    private RTToolbarImageButton mSuperscript;
    private RTToolbarImageButton mSubscript;
    private RTToolbarImageButton mAlignLeft;
    private RTToolbarImageButton mAlignCenter;
    private RTToolbarImageButton mAlignRight;
    private RTToolbarImageButton mBullet;
    private RTToolbarImageButton mNumber;

    /*
     * The Spinners and their SpinnerAdapters
     */
    private Spinner mFont;
    private SpinnerItemAdapter<FontSpinnerItem> mFontAdapter;

    private Spinner mFontSize;
    private SpinnerItemAdapter<FontSizeSpinnerItem> mFontSizeAdapter;

    private Spinner mFontColor;
    private SpinnerItemAdapter<? extends ColorSpinnerItem> mFontColorAdapter;

    private Spinner mBGColor;
    private SpinnerItemAdapter<? extends ColorSpinnerItem> mBGColorAdapter;

    private int mCustomColorFont = Color.BLACK;
    private int mCustomColorBG = Color.BLACK;

    private int mPickerId = -1;
    private ColorPickerListener mColorPickerListener;

    // ****************************************** Initialize Methods *******************************************

    public HorizontalRTToolbar(Context context) {
        super(context);
        init();
    }

    public HorizontalRTToolbar(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public HorizontalRTToolbar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        synchronized (sIdCounter) {
            mId = sIdCounter.getAndIncrement();
        }
        SetColorPickerListenerEvent.setListener(mPickerId, mColorPickerListener);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        // configure regular action buttons
        mBold = initImageButton(R.id.toolbar_bold);
        mItalic = initImageButton(R.id.toolbar_italic);
        mUnderline = initImageButton(R.id.toolbar_underline);
        mStrikethrough = initImageButton(R.id.toolbar_strikethrough);
        mSuperscript = initImageButton(R.id.toolbar_superscript);
        mSubscript = initImageButton(R.id.toolbar_subscript);
        mAlignLeft = initImageButton(R.id.toolbar_align_left);
        mAlignCenter = initImageButton(R.id.toolbar_align_center);
        mAlignRight = initImageButton(R.id.toolbar_align_right);
        mBullet = initImageButton(R.id.toolbar_bullet);
        mNumber = initImageButton(R.id.toolbar_number);

        initImageButton(R.id.toolbar_inc_indent);
        initImageButton(R.id.toolbar_dec_indent);
        initImageButton(R.id.toolbar_link);
        initImageButton(R.id.toolbar_image);
        initImageButton(R.id.toolbar_undo);
        initImageButton(R.id.toolbar_redo);
        initImageButton(R.id.toolbar_clear);

        // enable/disable capture picture depending on whether the device
        // has a camera or not
        PackageManager packageMgr = getContext().getPackageManager();
        if (packageMgr.hasSystemFeature(PackageManager.FEATURE_CAMERA)) {
            initImageButton(R.id.toolbar_image_capture);
        } else {
            View imageCapture = findViewById(R.id.toolbar_image_capture);
            if (imageCapture != null) imageCapture.setVisibility(View.GONE);
        }

        // configure font button
        mFont = (Spinner) findViewById(R.id.toolbar_font);
        mFontAdapter = createDropDownNav(mFont,
                R.layout.rte_toolbar_font_spinner,
                R.layout.rte_toolbar_spinner_item,
                getFontItems(), mFontListener);

        // configure font size button
        mFontSize = (Spinner) findViewById(R.id.toolbar_fontsize);
        mFontSizeAdapter = createDropDownNav(mFontSize,
                R.layout.rte_toolbar_fontsize_spinner,
                R.layout.rte_toolbar_spinner_item,
                getTextSizeItems(), mFontSizeListener);

        // configure font color button
        mFontColor = (Spinner) findViewById(R.id.toolbar_fontcolor);
        mFontColorAdapter = createDropDownNav(mFontColor,
                R.layout.rte_toolbar_fontcolor_spinner,
                R.layout.rte_toolbar_fontcolor_spinner_item,
                getFontColorItems(), mFontColorListener);

        // configure bg color button
        mBGColor = (Spinner) findViewById(R.id.toolbar_bgcolor);
        mBGColorAdapter = createDropDownNav(mBGColor,
                R.layout.rte_toolbar_bgcolor_spinner,
                R.layout.rte_toolbar_bgcolor_spinner_item,
                getBGColorItems(), mBGColorListener);
    }

    private RTToolbarImageButton initImageButton(int id) {
        RTToolbarImageButton button = (RTToolbarImageButton) findViewById(id);
        if (button != null) {
            button.setOnClickListener(this);
        }
        return button;
    }

    private SpinnerItems<FontSpinnerItem> getFontItems() {
        /*
         * Retrieve the fonts.
         */
        SortedSet<RTTypeface> fonts = FontManager.getFonts(getContext());

        /*
         * Create the spinner items
         */
        SpinnerItems<FontSpinnerItem> spinnerItems = new SpinnerItems<FontSpinnerItem>();
        spinnerItems.add(new FontSpinnerItem(null));        // empty element
        for (RTTypeface typeface : fonts) {
            spinnerItems.add(new FontSpinnerItem(typeface));
        }

        return spinnerItems;
    }

    private SpinnerItems<FontSizeSpinnerItem> getTextSizeItems() {
        SpinnerItems<FontSizeSpinnerItem> spinnerItems = new SpinnerItems<FontSizeSpinnerItem>();
        Resources res = getResources();

        // empty size
        spinnerItems.add(new FontSizeSpinnerItem(-1, "", true));

        // regular sizes
        String[] fontSizeEntries = res.getStringArray(R.array.rte_toolbar_fontsizes_entries);
        int[] fontSizeValues = res.getIntArray(R.array.rte_toolbar_fontsizes_values);
        for (int i = 0; i < fontSizeEntries.length; i++) {
            spinnerItems.add(new FontSizeSpinnerItem(fontSizeValues[i], fontSizeEntries[i], false));
        }

        return spinnerItems;
    }

    private SpinnerItems<FontColorSpinnerItem> getFontColorItems() {
        SpinnerItems<FontColorSpinnerItem> spinnerItems = new SpinnerItems<FontColorSpinnerItem>();
        Context context = getContext();

        // empty color
        String name = context.getString(R.string.rte_toolbar_color_text);
        FontColorSpinnerItem spinnerItem = new FontColorSpinnerItem(mCustomColorFont, name, true, false);
        spinnerItems.add(spinnerItem);

        // regular colors
        for (String fontColor : getResources().getStringArray(R.array.rte_toolbar_fontcolors_values)) {
            int color = Integer.parseInt(fontColor, 16);
            spinnerItem = new FontColorSpinnerItem(color, name, false, false);
            spinnerItems.add(spinnerItem);
        }

        // custom color
        name = context.getString(R.string.rte_toolbar_color_custom);
        spinnerItem = new FontColorSpinnerItem(mCustomColorFont, name, false, true);
        spinnerItems.add(spinnerItem);

        return spinnerItems;
    }

    private SpinnerItems<BGColorSpinnerItem> getBGColorItems() {
        SpinnerItems<BGColorSpinnerItem> spinnerItems = new SpinnerItems<BGColorSpinnerItem>();
        Context context = getContext();

        // empty color
        String name = context.getString(R.string.rte_toolbar_color_text);
        BGColorSpinnerItem spinnerItem = new BGColorSpinnerItem(mCustomColorFont, name, true, false);
        spinnerItems.add(spinnerItem);

        // regular colors
        for (String fontColor : getResources().getStringArray(R.array.rte_toolbar_fontcolors_values)) {
            int color = Integer.parseInt(fontColor, 16);
            spinnerItem = new BGColorSpinnerItem(color, name, false, false);
            spinnerItems.add(spinnerItem);
        }

        // custom color
        name = context.getString(R.string.rte_toolbar_color_custom);
        spinnerItem = new BGColorSpinnerItem(mCustomColorFont, name, false, true);
        spinnerItems.add(spinnerItem);

        return spinnerItems;
    }

    private <T extends SpinnerItem> SpinnerItemAdapter<T> createDropDownNav(Spinner spinner, int spinnerId, int spinnerItemId,
                                                                            SpinnerItems<T> spinnerItems,
                                                                            final DropDownNavListener<T> listener) {
        if (spinner != null) {
            Context context = getContext();

            // create custom adapter
            final SpinnerItemAdapter<T> dropDownNavAdapter = new SpinnerItemAdapter<T>(context, spinnerItems, spinnerId, spinnerItemId);

            // configure spinner
            spinner.setPadding(spinner.getPaddingLeft(), 0, spinner.getPaddingRight(), 0);
            spinner.setAdapter(dropDownNavAdapter);
            // we need this because otherwise the first item will be selected by
            // default and the OnItemSelectedListener won't get called
            spinner.setSelection(spinnerItems.getSelectedItem());
            spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
                private AtomicBoolean mFirstCall = new AtomicBoolean(true);

                @Override
                public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                    if (!mFirstCall.getAndSet(false) && dropDownNavAdapter.getSelectedItem() != position) {
                        listener.onItemSelected(dropDownNavAdapter.getItem(position), position);
                    }
                    dropDownNavAdapter.setSelectedItem(position);
                }

                @Override
                public void onNothingSelected(AdapterView<?> parent) {
                }
            });

            return dropDownNavAdapter;
        }

        return null;
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        if (mColorPickerListener != null && mPickerId != -1) {
            SetColorPickerListenerEvent.setListener(mPickerId, mColorPickerListener);
        }
    }

    // ****************************************** RTToolbar Methods *******************************************

    @Override
    public void setToolbarContainer(ViewGroup toolbarContainer) {
        mToolbarContainer = toolbarContainer;
    }

    @Override
    public ViewGroup getToolbarContainer() {
        return mToolbarContainer == null ? this : mToolbarContainer;
    }

    @Override
    public void setToolbarListener(RTToolbarListener listener) {
        mListener = listener;
    }

    @Override
    public void removeToolbarListener() {
        mListener = null;
    }

    @Override
    public int getId() {
        return mId;
    }

    @Override
    public void setBold(boolean enabled) {
        if (mBold != null) mBold.setChecked(enabled);
    }

    @Override
    public void setItalic(boolean enabled) {
        if (mItalic != null) mItalic.setChecked(enabled);
    }

    @Override
    public void setUnderline(boolean enabled) {
        if (mUnderline != null) mUnderline.setChecked(enabled);
    }

    @Override
    public void setStrikethrough(boolean enabled) {
        if (mStrikethrough != null) mStrikethrough.setChecked(enabled);
    }

    @Override
    public void setSuperscript(boolean enabled) {
        if (mSuperscript != null) mSuperscript.setChecked(enabled);
    }

    @Override
    public void setSubscript(boolean enabled) {
        if (mSubscript != null) mSubscript.setChecked(enabled);
    }

    @Override
    public void setBullet(boolean enabled) {
        if (mBullet != null) mBullet.setChecked(enabled);
    }

    @Override
    public void setNumber(boolean enabled) {
        if (mNumber != null) mNumber.setChecked(enabled);
    }

    @Override
    public void setAlignment(Layout.Alignment alignment) {
        if (mAlignLeft != null) mAlignLeft.setChecked(alignment == Layout.Alignment.ALIGN_NORMAL);
        if (mAlignCenter != null)
            mAlignCenter.setChecked(alignment == Layout.Alignment.ALIGN_CENTER);
        if (mAlignRight != null)
            mAlignRight.setChecked(alignment == Layout.Alignment.ALIGN_OPPOSITE);
    }

    @Override
    public void setFont(RTTypeface typeface) {
        if (mFont != null) {
            if (typeface != null) {
                for (int pos = 0; pos < mFontAdapter.getCount(); pos++) {
                    FontSpinnerItem item = mFontAdapter.getItem(pos);
                    if (typeface.equals(item.getTypeface())) {
                        mFontAdapter.setSelectedItem(pos);
                        mFont.setSelection(pos);
                        break;
                    }
                }
            }
            else {
                mFontAdapter.setSelectedItem(0);
                mFont.setSelection(0);
            }
        }
    }

    /**
     * Set the text size.
     *
     * @param size the text size, if -1 then no text size is set (e.g. when selection spans more than one text size)
     */
    @Override
    public void setFontSize(int size) {
        if (mFontSize != null) {
            if (size <= 0) {
                mFontSizeAdapter.updateSpinnerTitle("");
                mFontSizeAdapter.setSelectedItem(0);
                mFontSize.setSelection(0);
            } else {
                size = Helper.convertSpToPx(size);
                mFontSizeAdapter.updateSpinnerTitle(Integer.toString(size));
                for (int pos = 0; pos < mFontSizeAdapter.getCount(); pos++) {
                    FontSizeSpinnerItem item = mFontSizeAdapter.getItem(pos);
                    if (size == item.getFontSize()) {
                        mFontSizeAdapter.setSelectedItem(pos);
                        mFontSize.setSelection(pos);
                        break;
                    }
                }
            }
        }
    }

    @Override
    public void setFontColor(int color) {
        if (mFontColor != null) setFontColor(color, mFontColor, mFontColorAdapter);
    }

    @Override
    public void setBGColor(int color) {
        if (mBGColor != null) setFontColor(color, mBGColor, mBGColorAdapter);
    }

    @Override
    public void removeFontColor() {
        if (mFontColor != null) {
            mFontColorAdapter.setSelectedItem(0);
            mFontColor.setSelection(0);
        }
    }

    @Override
    public void removeBGColor() {
        if (mBGColor != null) {
            mBGColorAdapter.setSelectedItem(0);
            mBGColor.setSelection(0);
        }
    }

    private void setFontColor(int color, Spinner spinner, SpinnerItemAdapter<? extends ColorSpinnerItem> adapter) {
        int color2Compare = color & 0xffffff;
        for (int pos = 0; pos < adapter.getCount(); pos++) {
            ColorSpinnerItem item = adapter.getItem(pos);
            if (!item.isEmpty() && color2Compare == (item.getColor() & 0xffffff)) {
                adapter.setSelectedItem(pos);
                spinner.setSelection(pos);
                break;
            }
        }
    }

    // ****************************************** Item Selected Methods *******************************************

    interface DropDownNavListener<T extends SpinnerItem> {
        void onItemSelected(T spinnerItem, int position);
    }

    private DropDownNavListener<FontSpinnerItem> mFontListener = new DropDownNavListener<FontSpinnerItem>() {
        @Override
        public void onItemSelected(FontSpinnerItem spinnerItem, int position) {
            RTTypeface typeface = spinnerItem.getTypeface();
            mListener.onEffectSelected(Effects.TYPEFACE, typeface);
        }
    };

    private DropDownNavListener<FontSizeSpinnerItem> mFontSizeListener = new DropDownNavListener<FontSizeSpinnerItem>() {
        @Override
        public void onItemSelected(FontSizeSpinnerItem spinnerItem, int position) {
            int size = spinnerItem.getFontSize();
            mFontSizeAdapter.updateSpinnerTitle(spinnerItem.isEmpty() ? "" : Integer.toString(size));
            size = Helper.convertPxToSp(size);
            mListener.onEffectSelected(Effects.FONTSIZE, size);
        }
    };

    private DropDownNavListener<FontColorSpinnerItem> mFontColorListener = new DropDownNavListener<FontColorSpinnerItem>() {
        @Override
        public void onItemSelected(final FontColorSpinnerItem spinnerItem, int position) {
            if (spinnerItem.isCustom()) {
                mColorPickerListener = new ColorPickerListener() {
                    @Override
                    public void onColorChanged(int color) {
                        mCustomColorFont = color;
                        spinnerItem.setColor(color);
                        mFontColorAdapter.notifyDataSetChanged();
                        if (mListener != null) {
                            mListener.onEffectSelected(Effects.FONTCOLOR, color);
                        }
                    }
                    @Override
                    public void onDialogClosing() {
                        mPickerId = -1;
                    }
                };
                mPickerId = new ColorPickerDialog(getContext(), mCustomColorFont, false).show();
                SetColorPickerListenerEvent.setListener(mPickerId, mColorPickerListener);
            } else if (mListener != null) {
                Integer color = spinnerItem.isEmpty() ? null : spinnerItem.getColor();
                mListener.onEffectSelected(Effects.FONTCOLOR, color);
            }
        }
    };

    private DropDownNavListener<BGColorSpinnerItem> mBGColorListener = new DropDownNavListener<BGColorSpinnerItem>() {
        @Override
        public void onItemSelected(final BGColorSpinnerItem spinnerItem, int position) {
            if (spinnerItem.isCustom()) {
                mColorPickerListener = new ColorPickerListener() {
                    @Override
                    public void onColorChanged(int color) {
                        mCustomColorBG = color;
                        spinnerItem.setColor(color);
                        mBGColorAdapter.notifyDataSetChanged();
                        if (mListener != null) {
                            mListener.onEffectSelected(Effects.BGCOLOR, color);
                        }
                    }
                    public void onDialogClosing() {
                        mPickerId = -1;
                    }
                };
                mPickerId = new ColorPickerDialog(getContext(), mCustomColorBG, false).show();
                SetColorPickerListenerEvent.setListener(mPickerId, mColorPickerListener);
            } else if (mListener != null) {
                Integer color = spinnerItem.isEmpty() ? null : spinnerItem.getColor();
                mListener.onEffectSelected(Effects.BGCOLOR, color);
            }
        }
    };

    @Override
    public void onClick(View v) {
        if (mListener != null) {

            int id = v.getId();
            if (id == R.id.toolbar_bold) {
                mBold.setChecked(!mBold.isChecked());
                mListener.onEffectSelected(Effects.BOLD, mBold.isChecked());
            }

            else if (id == R.id.toolbar_italic) {
                mItalic.setChecked(!mItalic.isChecked());
                mListener.onEffectSelected(Effects.ITALIC, mItalic.isChecked());
            }

            else if (id == R.id.toolbar_underline) {
                mUnderline.setChecked(!mUnderline.isChecked());
                mListener.onEffectSelected(Effects.UNDERLINE, mUnderline.isChecked());
            }

            else if (id == R.id.toolbar_strikethrough) {
                mStrikethrough.setChecked(!mStrikethrough.isChecked());
                mListener.onEffectSelected(Effects.STRIKETHROUGH, mStrikethrough.isChecked());
            }

            else if (id == R.id.toolbar_superscript) {
                mSuperscript.setChecked(!mSuperscript.isChecked());
                mListener.onEffectSelected(Effects.SUPERSCRIPT, mSuperscript.isChecked());
                if (mSuperscript.isChecked() && mSubscript != null) {
                    mSubscript.setChecked(false);
                    mListener.onEffectSelected(Effects.SUBSCRIPT, mSubscript.isChecked());
                }
            }

            else if (id == R.id.toolbar_subscript) {
                mSubscript.setChecked(!mSubscript.isChecked());
                mListener.onEffectSelected(Effects.SUBSCRIPT, mSubscript.isChecked());
                if (mSubscript.isChecked() && mSuperscript != null) {
                    mSuperscript.setChecked(false);
                    mListener.onEffectSelected(Effects.SUPERSCRIPT, mSuperscript.isChecked());
                }
            }

            else if (id == R.id.toolbar_align_left) {
                if (mAlignLeft != null) mAlignLeft.setChecked(true);
                if (mAlignCenter != null) mAlignCenter.setChecked(false);
                if (mAlignRight != null) mAlignRight.setChecked(false);
                mListener.onEffectSelected(Effects.ALIGNMENT, Layout.Alignment.ALIGN_NORMAL);
            }

            else if (id == R.id.toolbar_align_center) {
                if (mAlignLeft != null) mAlignLeft.setChecked(false);
                if (mAlignCenter != null) mAlignCenter.setChecked(true);
                if (mAlignRight != null) mAlignRight.setChecked(false);
                mListener.onEffectSelected(Effects.ALIGNMENT, Layout.Alignment.ALIGN_CENTER);
            }

            else if (id == R.id.toolbar_align_right) {
                if (mAlignLeft != null) mAlignLeft.setChecked(false);
                if (mAlignCenter != null) mAlignCenter.setChecked(false);
                if (mAlignRight != null) mAlignRight.setChecked(true);
                mListener.onEffectSelected(Effects.ALIGNMENT, Layout.Alignment.ALIGN_OPPOSITE);
            }

            else if (id == R.id.toolbar_bullet) {
                mBullet.setChecked(!mBullet.isChecked());
                boolean isChecked = mBullet.isChecked();
                mListener.onEffectSelected(Effects.BULLET, isChecked);
                if (isChecked && mNumber != null) {
                    mNumber.setChecked(false);    // numbers will be removed by the NumberEffect.applyToSelection
                }
            }

            else if (id == R.id.toolbar_number) {
                mNumber.setChecked(!mNumber.isChecked());
                boolean isChecked = mNumber.isChecked();
                mListener.onEffectSelected(Effects.NUMBER, isChecked);
                if (isChecked && mBullet != null) {
                    mBullet.setChecked(false);    // bullets will be removed by the BulletEffect.applyToSelection
                }
            }

            else if (id == R.id.toolbar_inc_indent) {
                mListener.onEffectSelected(Effects.INDENTATION, Helper.getLeadingMarging());
            }

            else if (id == R.id.toolbar_dec_indent) {
                mListener.onEffectSelected(Effects.INDENTATION, -Helper.getLeadingMarging());
            }

            else if (id == R.id.toolbar_link) {
                mListener.onCreateLink();
            }

            else if (id == R.id.toolbar_image) {
                mListener.onPickImage();
            }

            else if (id == R.id.toolbar_image_capture) {
                mListener.onCaptureImage();
            }

            else if (id == R.id.toolbar_clear) {
                mListener.onClearFormatting();
            }

            else if (id == R.id.toolbar_undo) {
                mListener.onUndo();
            }

            else if (id == R.id.toolbar_redo) {
                mListener.onRedo();
            }
        }
    }

}