// 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.
//
//
// The TagSoup parser

package com.onegravity.rteditor.converter.tagsoup;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.DTDHandler;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.XMLReader;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.DefaultHandler;

import android.annotation.SuppressLint;

/**
 * The SAX parser class.
 */
public class Parser extends DefaultHandler implements ScanHandler, XMLReader, LexicalHandler {

    // XMLReader implementation

    private ContentHandler theContentHandler = this;
    private LexicalHandler theLexicalHandler = this;
    private DTDHandler theDTDHandler = this;
    private ErrorHandler theErrorHandler = this;
    private EntityResolver theEntityResolver = this;
    private Schema theSchema;
    private Scanner theScanner;
    private AutoDetector theAutoDetector;

    // Default values for feature flags

    private static boolean DEFAULT_NAMESPACES = true;
    private static boolean DEFAULT_IGNORE_BOGONS = false;
    private static boolean DEFAULT_BOGONS_EMPTY = false;
    private static boolean DEFAULT_ROOT_BOGONS = true;
    private static boolean DEFAULT_DEFAULT_ATTRIBUTES = true;
    private static boolean DEFAULT_TRANSLATE_COLONS = false;
    private static boolean DEFAULT_RESTART_ELEMENTS = true;
    private static boolean DEFAULT_IGNORABLE_WHITESPACE = false;
    private static boolean DEFAULT_CDATA_ELEMENTS = true;

    // Feature flags.

    private boolean namespaces = DEFAULT_NAMESPACES;
    private boolean ignoreBogons = DEFAULT_IGNORE_BOGONS;
    private boolean bogonsEmpty = DEFAULT_BOGONS_EMPTY;
    private boolean rootBogons = DEFAULT_ROOT_BOGONS;
    private boolean defaultAttributes = DEFAULT_DEFAULT_ATTRIBUTES;
    private boolean translateColons = DEFAULT_TRANSLATE_COLONS;
    private boolean restartElements = DEFAULT_RESTART_ELEMENTS;
    private boolean ignorableWhitespace = DEFAULT_IGNORABLE_WHITESPACE;
    private boolean CDATAElements = DEFAULT_CDATA_ELEMENTS;

    /**
     * A value of "true" indicates namespace URIs and unprefixed local names for
     * element and attribute names will be available.
     */
    public final static String namespacesFeature = "http://xml.org/sax/features/namespaces";

    /**
     * A value of "true" indicates that XML qualified names (with prefixes) and
     * attributes (including xmlns* attributes) will be available. We don't
     * support this value.
     */
    public final static String namespacePrefixesFeature = "http://xml.org/sax/features/namespace-prefixes";

    /**
     * Reports whether this parser processes external general entities (it
     * doesn't).
     */
    public final static String externalGeneralEntitiesFeature = "http://xml.org/sax/features/external-general-entities";

    /**
     * Reports whether this parser processes external parameter entities (it
     * doesn't).
     */
    public final static String externalParameterEntitiesFeature = "http://xml.org/sax/features/external-parameter-entities";

    /**
     * May be examined only during a parse, after the startDocument() callback
     * has been completed; read-only. The value is true if the document
     * specified standalone="yes" in its XML declaration, and otherwise is
     * false. (It's always false.)
     */
    public final static String isStandaloneFeature = "http://xml.org/sax/features/is-standalone";

    /**
     * A value of "true" indicates that the LexicalHandler will report the
     * beginning and end of parameter entities (it won't).
     */
    public final static String lexicalHandlerParameterEntitiesFeature = "http://xml.org/sax/features/lexical-handler/parameter-entities";

    /**
     * A value of "true" indicates that system IDs in declarations will be
     * absolutized (relative to their base URIs) before reporting. (This returns
     * true but doesn't actually do anything.)
     */
    public final static String resolveDTDURIsFeature = "http://xml.org/sax/features/resolve-dtd-uris";

    /**
     * Has a value of "true" if all XML names (for elements, prefixes,
     * attributes, entities, notations, and local names), as well as Namespace
     * URIs, will have been interned using java.lang.String.intern. This
     * supports fast testing of equality/inequality against string constants,
     * rather than forcing slower calls to String.equals(). (We always intern.)
     */
    public final static String stringInterningFeature = "http://xml.org/sax/features/string-interning";

    /**
     * Returns "true" if the Attributes objects passed by this parser in
     * ContentHandler.startElement() implement the org.xml.sax.ext.Attributes2
     * interface. (They don't.)
     */

    public final static String useAttributes2Feature = "http://xml.org/sax/features/use-attributes2";

    /**
     * Returns "true" if the Locator objects passed by this parser in
     * ContentHandler.setDocumentLocator() implement the
     * org.xml.sax.ext.Locator2 interface. (They don't.)
     */
    public final static String useLocator2Feature = "http://xml.org/sax/features/use-locator2";

    /**
     * Returns "true" if, when setEntityResolver is given an object implementing
     * the org.xml.sax.ext.EntityResolver2 interface, those new methods will be
     * used. (They won't be.)
     */
    public final static String useEntityResolver2Feature = "http://xml.org/sax/features/use-entity-resolver2";

    /**
     * Controls whether the parser is reporting all validity errors (We don't
     * report any validity errors.)
     */
    public final static String validationFeature = "http://xml.org/sax/features/validation";

    /**
     * Controls whether the parser reports Unicode normalization errors as
     * described in section 2.13 and Appendix B of the XML 1.1 Recommendation.
     * (We don't normalize.)
     */
    public final static String unicodeNormalizationCheckingFeature = "http://xml.org/sax/features/unicode-normalization-checking";

    /**
     * Controls whether, when the namespace-prefixes feature is set, the parser
     * treats namespace declaration attributes as being in the
     * http://www.w3.org/2000/xmlns/ namespace. (It doesn't.)
     */
    public final static String xmlnsURIsFeature = "http://xml.org/sax/features/xmlns-uris";

    /**
     * Returns "true" if the parser supports both XML 1.1 and XML 1.0. (Always
     * false.)
     */
    public final static String XML11Feature = "http://xml.org/sax/features/xml-1.1";

    /**
     * A value of "true" indicates that the parser will ignore unknown elements.
     */
    public final static String ignoreBogonsFeature = "http://www.ccil.org/~cowan/tagsoup/features/ignore-bogons";

    /**
     * A value of "true" indicates that the parser will give unknown elements a
     * content model of EMPTY; a value of "false", a content model of ANY.
     */
    public final static String bogonsEmptyFeature = "http://www.ccil.org/~cowan/tagsoup/features/bogons-empty";

    /**
     * A value of "true" indicates that the parser will allow unknown elements
     * to be the root element.
     */
    public final static String rootBogonsFeature = "http://www.ccil.org/~cowan/tagsoup/features/root-bogons";

    /**
     * A value of "true" indicates that the parser will return default attribute
     * values for missing attributes that have default values.
     */
    public final static String defaultAttributesFeature = "http://www.ccil.org/~cowan/tagsoup/features/default-attributes";

    /**
     * A value of "true" indicates that the parser will translate colons into
     * underscores in names.
     */
    public final static String translateColonsFeature = "http://www.ccil.org/~cowan/tagsoup/features/translate-colons";

    /**
     * A value of "true" indicates that the parser will attempt to restart the
     * restartable elements.
     */
    public final static String restartElementsFeature = "http://www.ccil.org/~cowan/tagsoup/features/restart-elements";

    /**
     * A value of "true" indicates that the parser will transmit whitespace in
     * element-only content via the SAX ignorableWhitespace callback. Normally
     * this is not done, because HTML is an SGML application and SGML suppresses
     * such whitespace.
     */
    public final static String ignorableWhitespaceFeature = "http://www.ccil.org/~cowan/tagsoup/features/ignorable-whitespace";

    /**
     * A value of "true" indicates that the parser will treat CDATA elements
     * specially. Normally true, since the input is by default HTML.
     */
    public final static String CDATAElementsFeature = "http://www.ccil.org/~cowan/tagsoup/features/cdata-elements";

    /**
     * Used to see some syntax events that are essential in some applications:
     * comments, CDATA delimiters, selected general entity inclusions, and the
     * start and end of the DTD (and declaration of document element name). The
     * Object must implement org.xml.sax.ext.LexicalHandler.
     */
    public final static String lexicalHandlerProperty = "http://xml.org/sax/properties/lexical-handler";

    /**
     * Specifies the Scanner object this Parser uses.
     */
    public final static String scannerProperty = "http://www.ccil.org/~cowan/tagsoup/properties/scanner";

    /**
     * Specifies the Schema object this Parser uses.
     */
    public final static String schemaProperty = "http://www.ccil.org/~cowan/tagsoup/properties/schema";

    /**
     * Specifies the AutoDetector (for encoding detection) this Parser uses.
     */
    public final static String autoDetectorProperty = "http://www.ccil.org/~cowan/tagsoup/properties/auto-detector";

    // Due to sucky Java order of initialization issues, these
    // entries are maintained separately from the initial values of
    // the corresponding instance variables, but care must be taken
    // to keep them in sync.

    private HashMap<String, Boolean> theFeatures = new HashMap<String, Boolean>();

    {
        theFeatures.put(namespacesFeature, truthValue(DEFAULT_NAMESPACES));
        theFeatures.put(namespacePrefixesFeature, Boolean.FALSE);
        theFeatures.put(externalGeneralEntitiesFeature, Boolean.FALSE);
        theFeatures.put(externalParameterEntitiesFeature, Boolean.FALSE);
        theFeatures.put(isStandaloneFeature, Boolean.FALSE);
        theFeatures.put(lexicalHandlerParameterEntitiesFeature, Boolean.FALSE);
        theFeatures.put(resolveDTDURIsFeature, Boolean.TRUE);
        theFeatures.put(stringInterningFeature, Boolean.TRUE);
        theFeatures.put(useAttributes2Feature, Boolean.FALSE);
        theFeatures.put(useLocator2Feature, Boolean.FALSE);
        theFeatures.put(useEntityResolver2Feature, Boolean.FALSE);
        theFeatures.put(validationFeature, Boolean.FALSE);
        theFeatures.put(xmlnsURIsFeature, Boolean.FALSE);
        theFeatures.put(xmlnsURIsFeature, Boolean.FALSE);
        theFeatures.put(XML11Feature, Boolean.FALSE);
        theFeatures.put(ignoreBogonsFeature, truthValue(DEFAULT_IGNORE_BOGONS));
        theFeatures.put(bogonsEmptyFeature, truthValue(DEFAULT_BOGONS_EMPTY));
        theFeatures.put(rootBogonsFeature, truthValue(DEFAULT_ROOT_BOGONS));
        theFeatures.put(defaultAttributesFeature, truthValue(DEFAULT_DEFAULT_ATTRIBUTES));
        theFeatures.put(translateColonsFeature, truthValue(DEFAULT_TRANSLATE_COLONS));
        theFeatures.put(restartElementsFeature, truthValue(DEFAULT_RESTART_ELEMENTS));
        theFeatures.put(ignorableWhitespaceFeature, truthValue(DEFAULT_IGNORABLE_WHITESPACE));
        theFeatures.put(CDATAElementsFeature, truthValue(DEFAULT_CDATA_ELEMENTS));
    }

    // Private clone of Boolean.valueOf that is guaranteed to return
    // Boolean.TRUE or Boolean.FALSE
    private static Boolean truthValue(boolean b) {
        return b ? Boolean.TRUE : Boolean.FALSE;
    }

    @Override
    public boolean getFeature(String name) throws SAXNotRecognizedException, SAXNotSupportedException {
        Boolean b = (Boolean) theFeatures.get(name);
        if (b == null) {
            throw new SAXNotRecognizedException("Unknown feature " + name);
        }
        return b.booleanValue();
    }

    @Override
    public void setFeature(String name, boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
        Boolean b = (Boolean) theFeatures.get(name);
        if (b == null) {
            throw new SAXNotRecognizedException("Unknown feature " + name);
        }
        if (value)
            theFeatures.put(name, Boolean.TRUE);
        else
            theFeatures.put(name, Boolean.FALSE);

        if (name.equals(namespacesFeature))
            namespaces = value;
        else if (name.equals(ignoreBogonsFeature))
            ignoreBogons = value;
        else if (name.equals(bogonsEmptyFeature))
            bogonsEmpty = value;
        else if (name.equals(rootBogonsFeature))
            rootBogons = value;
        else if (name.equals(defaultAttributesFeature))
            defaultAttributes = value;
        else if (name.equals(translateColonsFeature))
            translateColons = value;
        else if (name.equals(restartElementsFeature))
            restartElements = value;
        else if (name.equals(ignorableWhitespaceFeature))
            ignorableWhitespace = value;
        else if (name.equals(CDATAElementsFeature))
            CDATAElements = value;
    }

    @Override
    public Object getProperty(String name) throws SAXNotRecognizedException,
            SAXNotSupportedException {
        if (name.equals(lexicalHandlerProperty)) {
            return theLexicalHandler == this ? null : theLexicalHandler;
        } else if (name.equals(scannerProperty)) {
            return theScanner;
        } else if (name.equals(schemaProperty)) {
            return theSchema;
        } else if (name.equals(autoDetectorProperty)) {
            return theAutoDetector;
        } else {
            throw new SAXNotRecognizedException("Unknown property " + name);
        }
    }

    @Override
    public void setProperty(String name, Object value) throws SAXNotRecognizedException, SAXNotSupportedException {
        if (name.equals(lexicalHandlerProperty)) {
            if (value == null) {
                theLexicalHandler = this;
            } else if (value instanceof LexicalHandler) {
                theLexicalHandler = (LexicalHandler) value;
            } else {
                throw new SAXNotSupportedException("Your lexical handler is not a LexicalHandler");
            }
        } else if (name.equals(scannerProperty)) {
            if (value instanceof Scanner) {
                theScanner = (Scanner) value;
            } else {
                throw new SAXNotSupportedException("Your scanner is not a Scanner");
            }
        } else if (name.equals(schemaProperty)) {
            if (value instanceof Schema) {
                theSchema = (Schema) value;
            } else {
                throw new SAXNotSupportedException("Your schema is not a Schema");
            }
        } else if (name.equals(autoDetectorProperty)) {
            if (value instanceof AutoDetector) {
                theAutoDetector = (AutoDetector) value;
            } else {
                throw new SAXNotSupportedException("Your auto-detector is not an AutoDetector");
            }
        } else {
            throw new SAXNotRecognizedException("Unknown property " + name);
        }
    }

    @Override
    public void setEntityResolver(EntityResolver resolver) {
        theEntityResolver = (resolver == null) ? this : resolver;
    }

    @Override
    public EntityResolver getEntityResolver() {
        return (theEntityResolver == this) ? null : theEntityResolver;
    }

    @Override
    public void setDTDHandler(DTDHandler handler) {
        theDTDHandler = (handler == null) ? this : handler;
    }

    @Override
    public DTDHandler getDTDHandler() {
        return (theDTDHandler == this) ? null : theDTDHandler;
    }

    @Override
    public void setContentHandler(ContentHandler handler) {
        theContentHandler = (handler == null) ? this : handler;
    }

    @Override
    public ContentHandler getContentHandler() {
        return (theContentHandler == this) ? null : theContentHandler;
    }

    @Override
    public void setErrorHandler(ErrorHandler handler) {
        theErrorHandler = (handler == null) ? this : handler;
    }

    @Override
    public ErrorHandler getErrorHandler() {
        return (theErrorHandler == this) ? null : theErrorHandler;
    }

    @Override
    public void parse(String systemid) throws IOException, SAXException {
        parse(new InputSource(systemid));
    }

    @Override
    public synchronized void parse(InputSource input) throws IOException, SAXException {
        setup();

        Reader r = getReader(input);
        theContentHandler.startDocument();
        theScanner.resetDocumentLocator(input.getPublicId(), input.getSystemId());
        if (theScanner instanceof Locator) {
            theContentHandler.setDocumentLocator((Locator) theScanner);
        }

        if (!(theSchema.getURI().equals(""))) {
            theContentHandler.startPrefixMapping(theSchema.getPrefix(), theSchema.getURI());
        }
        theScanner.scan(r, this);
    }

    // Sets up instance variables that haven't been set by setFeature
    private void setup() {
        if (theSchema == null) theSchema = new HTMLSchema();
        if (theScanner == null) theScanner = new HTMLScanner();
        if (theAutoDetector == null) {
            theAutoDetector = new AutoDetector() {
                public Reader autoDetectingReader(InputStream i) {
                    return new InputStreamReader(i);
                }
            };
        }
        theStack = new Element(theSchema.getElementType("<root>"), defaultAttributes);
        thePCDATA = new Element(theSchema.getElementType("<pcdata>"), defaultAttributes);
        theNewElement = null;
        theAttributeName = null;
        thePITarget = null;
        theSaved = null;
        theEntity = 0;
        virginStack = true;
        theDoctypeName = theDoctypePublicId = theDoctypeSystemId = null;
    }

    // Return a Reader based on the contents of an InputSource
    // Buffer both the InputStream and the Reader
    private Reader getReader(InputSource s) throws SAXException, IOException {
        Reader r = s.getCharacterStream();
        InputStream i = s.getByteStream();
        String encoding = s.getEncoding();
        String publicid = s.getPublicId();
        String systemid = s.getSystemId();
        if (r == null) {
            if (i == null)
                i = getInputStream(publicid, systemid);
            // i = new BufferedInputStream(i);
            if (encoding == null) {
                r = theAutoDetector.autoDetectingReader(i);
            } else {
                try {
                    r = new InputStreamReader(i, encoding);
                } catch (UnsupportedEncodingException e) {
                    r = new InputStreamReader(i);
                }
            }
        }
        // r = new BufferedReader(r);
        return r;
    }

    // Get an InputStream based on a publicid and a systemid
    private InputStream getInputStream(String publicid, String systemid) throws IOException, SAXException {
        URL basis = new URL("file", "", System.getProperty("user.dir") + "/.");
        URL url = new URL(basis, systemid);
        URLConnection c = url.openConnection();
        return c.getInputStream();
    }

    // We don't process publicids (who uses them anyhow?)

    // ScanHandler implementation

    private Element theNewElement = null;
    private String theAttributeName = null;
    private boolean theDoctypeIsPresent = false;
    private String theDoctypePublicId = null;
    private String theDoctypeSystemId = null;
    private String theDoctypeName = null;
    private String thePITarget = null;
    private Element theStack = null;
    private Element theSaved = null;
    private Element thePCDATA = null;
    private int theEntity = 0; // needs to support chars past U+FFFF

    @Override
    public void adup(char[] buff, int offset, int length) throws SAXException {
        if (theNewElement != null && theAttributeName != null) {
            theNewElement.setAttribute(theAttributeName, null, theAttributeName);
            theAttributeName = null;
        }
    }

    @SuppressLint("DefaultLocale")
    @Override
    public void aname(char[] buff, int offset, int length) throws SAXException {
        if (theNewElement != null) {
            // currently we don't rely on Schema to canonicalize attribute names.
            theAttributeName = makeName(buff, offset, length).toLowerCase();
        }
    }

    @Override
    public void aval(char[] buff, int offset, int length) throws SAXException {
        if (theNewElement != null && theAttributeName != null) {
            String value = new String(buff, offset, length);
            value = expandEntities(value);
            theNewElement.setAttribute(theAttributeName, null, value);
            theAttributeName = null;
        }
    }

    // Expand entity references in attribute values selectively.
    // Currently we expand a reference iff it is properly terminated
    // with a semicolon.
    private String expandEntities(String src) {
        int refStart = -1;
        int len = src.length();
        char[] dst = new char[len];
        int dstlen = 0;
        for (int i = 0; i < len; i++) {
            char ch = src.charAt(i);
            dst[dstlen++] = ch;
            if (ch == '&' && refStart == -1) {
                // start of a ref excluding &
                refStart = dstlen;
            } else if (refStart == -1) {
                // not in a ref
            } else if (Character.isLetter(ch) || Character.isDigit(ch)
                    || ch == '#') {
                // valid entity char
            } else if (ch == ';') {
                // properly terminated ref
                int ent = lookupEntity(dst, refStart, dstlen - refStart - 1);
                if (ent > 0xFFFF) {
                    ent -= 0x10000;
                    dst[refStart - 1] = (char) ((ent >> 10) + 0xD800);
                    dst[refStart] = (char) ((ent & 0x3FF) + 0xDC00);
                    dstlen = refStart + 1;
                } else if (ent != 0) {
                    dst[refStart - 1] = (char) ent;
                    dstlen = refStart;
                }
                refStart = -1;
            } else {
                // improperly terminated ref
                refStart = -1;
            }
        }
        return new String(dst, 0, dstlen);
    }

    @Override
    public void entity(char[] buff, int offset, int length) throws SAXException {
        theEntity = lookupEntity(buff, offset, length);
    }

    // Process numeric character references,
    // deferring to the schema for named ones.
    private int lookupEntity(char[] buff, int offset, int length) {
        int result = 0;
        if (length < 1)
            return result;
        // length) + "]");
        if (buff[offset] == '#') {
            if (length > 1
                    && (buff[offset + 1] == 'x' || buff[offset + 1] == 'X')) {
                try {
                    return Integer.parseInt(new String(buff, offset + 2,
                            length - 2), 16);
                } catch (NumberFormatException e) {
                    return 0;
                }
            }
            try {
                return Integer.parseInt(
                        new String(buff, offset + 1, length - 1), 10);
            } catch (NumberFormatException e) {
                return 0;
            }
        }
        return theSchema.getEntity(new String(buff, offset, length));
    }

    @Override
    public void eof(char[] buff, int offset, int length) throws SAXException {
        if (virginStack)
            rectify(thePCDATA);
        while (theStack.next() != null) {
            pop();
        }
        if (!(theSchema.getURI().equals("")))
            theContentHandler.endPrefixMapping(theSchema.getPrefix());
        theContentHandler.endDocument();
    }

    @Override
    public void etag(char[] buff, int offset, int length) throws SAXException {
        if (etag_cdata(buff, offset, length))
            return;
        etag_basic(buff, offset, length);
    }

    private static char[] etagchars = {'<', '/', '>'};

    private boolean etag_cdata(char[] buff, int offset, int length) throws SAXException {
        String currentName = theStack.name();
        // If this is a CDATA element and the tag doesn't match,
        // or isn't properly formed (junk after the name),
        // restart CDATA mode and process the tag as characters.
        if (CDATAElements && (theStack.flags() & Schema.F_CDATA) != 0) {
            boolean realTag = (length == currentName.length());
            if (realTag) {
                for (int i = 0; i < length; i++) {
                    if (Character.toLowerCase(buff[offset + i]) != Character
                            .toLowerCase(currentName.charAt(i))) {
                        realTag = false;
                        break;
                    }
                }
            }
            if (!realTag) {
                theContentHandler.characters(etagchars, 0, 2);
                theContentHandler.characters(buff, offset, length);
                theContentHandler.characters(etagchars, 2, 1);
                theScanner.startCDATA();
                return true;
            }
        }
        return false;
    }

    private void etag_basic(char[] buff, int offset, int length) throws SAXException {
        theNewElement = null;
        String name;
        if (length != 0) {
            // Canonicalize case of name
            name = makeName(buff, offset, length);
            ElementType type = theSchema.getElementType(name);
            if (type == null)
                return; // mysterious end-tag
            name = type.name();
        } else {
            name = theStack.name();
        }

        Element sp;
        boolean inNoforce = false;
        for (sp = theStack; sp != null; sp = sp.next()) {
            if (sp.name().equals(name))
                break;
            if ((sp.flags() & Schema.F_NOFORCE) != 0)
                inNoforce = true;
        }

        if (sp == null)
            return; // Ignore unknown etags
        if (sp.next() == null || sp.next().next() == null)
            return;
        if (inNoforce) { // inside an F_NOFORCE element?
            sp.preclose(); // preclose the matching element
        } else { // restartably pop everything above us
            while (theStack != sp) {
                restartablyPop();
            }
            pop();
        }
        // pop any preclosed elements now at the top
        while (theStack.isPreclosed()) {
            pop();
        }
        restart(null);
    }

    // Push restartables on the stack if possible
    // e is the next element to be started, if we know what it is
    private void restart(Element e) throws SAXException {
        while (theSaved != null && theStack.canContain(theSaved) && (e == null || theSaved.canContain(e))) {
            Element next = theSaved.next();
            push(theSaved);
            theSaved = next;
        }
    }

    // Pop the stack irrevocably
    private void pop() throws SAXException {
        if (theStack == null)
            return; // empty stack
        String name = theStack.name();
        String localName = theStack.localName();
        String namespace = theStack.namespace();
        String prefix = prefixOf(name);

        if (!namespaces)
            namespace = localName = "";
        theContentHandler.endElement(namespace, localName, name);
        if (foreign(prefix, namespace)) {
            theContentHandler.endPrefixMapping(prefix);
            // "] for elements to " + namespace);
        }
        Attributes atts = theStack.atts();
        for (int i = atts.getLength() - 1; i >= 0; i--) {
            String attNamespace = atts.getURI(i);
            String attPrefix = prefixOf(atts.getQName(i));
            if (foreign(attPrefix, attNamespace)) {
                theContentHandler.endPrefixMapping(attPrefix);
                // "] for attributes to " + attNamespace);
            }
        }
        theStack = theStack.next();
    }

    // Pop the stack restartably
    private void restartablyPop() throws SAXException {
        Element popped = theStack;
        pop();
        if (restartElements && (popped.flags() & Schema.F_RESTART) != 0) {
            popped.anonymize();
            popped.setNext(theSaved);
            theSaved = popped;
        }
    }

    // Push element onto stack
    private boolean virginStack = true;

    private void push(Element e) throws SAXException {
        String name = e.name();
        String localName = e.localName();
        String namespace = e.namespace();
        String prefix = prefixOf(name);

        e.clean();
        if (!namespaces)
            namespace = localName = "";
        if (virginStack && localName.equalsIgnoreCase(theDoctypeName)) {
            try {
                theEntityResolver.resolveEntity(theDoctypePublicId,
                        theDoctypeSystemId);
            } catch (IOException ew) {
            } // Can't be thrown for root I believe.
        }
        if (foreign(prefix, namespace)) {
            theContentHandler.startPrefixMapping(prefix, namespace);
            // + namespace);
        }
        Attributes atts = e.atts();
        int len = atts.getLength();
        for (int i = 0; i < len; i++) {
            String attNamespace = atts.getURI(i);
            String attPrefix = prefixOf(atts.getQName(i));
            if (foreign(attPrefix, attNamespace)) {
                theContentHandler.startPrefixMapping(attPrefix, attNamespace);
                // "] for attributes to " + attNamespace);
            }
        }
        theContentHandler.startElement(namespace, localName, name, e.atts());
        e.setNext(theStack);
        theStack = e;
        virginStack = false;
        if (CDATAElements && (theStack.flags() & Schema.F_CDATA) != 0) {
            theScanner.startCDATA();
        }
    }

    // Get the prefix from a QName
    private String prefixOf(String name) {
        int i = name.indexOf(':');
        String prefix = "";
        if (i != -1)
            prefix = name.substring(0, i);
        return prefix;
    }

    // Return true if we have a foreign name
    private boolean foreign(String prefix, String namespace) {
        // " for foreignness -- ");
        boolean foreign = !(prefix.equals("") || namespace.equals("") || namespace.equals(theSchema.getURI()));
        return foreign;
    }

    /**
     * Parsing the complete XML Document Type Definition is way too complex, but
     * for many simple cases we can extract something useful from it.
     * <p>
     * doctypedecl ::= '<!DOCTYPE' S Name (S ExternalID)? S? ('[' intSubset ']'
     * S?)? '>' DeclSep ::= PEReference | S intSubset ::= (markupdecl |
     * DeclSep)* markupdecl ::= elementdecl | AttlistDecl | EntityDecl |
     * NotationDecl | PI | Comment ExternalID ::= 'SYSTEM' S SystemLiteral |
     * 'PUBLIC' S PubidLiteral S SystemLiteral
     */
    @Override
    public void decl(char[] buff, int offset, int length) throws SAXException {
        String s = new String(buff, offset, length);
        String name = null;
        String systemid = null;
        String publicid = null;
        String[] v = split(s);
        if (v.length > 0 && "DOCTYPE".equalsIgnoreCase(v[0])) {
            if (theDoctypeIsPresent)
                return; // one doctype only!
            theDoctypeIsPresent = true;
            if (v.length > 1) {
                name = v[1];
                if (v.length > 3 && "SYSTEM".equals(v[2])) {
                    systemid = v[3];
                } else if (v.length > 3 && "PUBLIC".equals(v[2])) {
                    publicid = v[3];
                    if (v.length > 4) {
                        systemid = v[4];
                    } else {
                        systemid = "";
                    }
                }
            }
        }
        publicid = trimquotes(publicid);
        systemid = trimquotes(systemid);
        if (name != null) {
            publicid = cleanPublicid(publicid);
            theLexicalHandler.startDTD(name, publicid, systemid);
            theLexicalHandler.endDTD();
            theDoctypeName = name;
            theDoctypePublicId = publicid;
            if (theScanner instanceof Locator) { // Must resolve systemid
                theDoctypeSystemId = ((Locator) theScanner).getSystemId();
                try {
                    theDoctypeSystemId = new URL(new URL(theDoctypeSystemId), systemid).toString();
                } catch (Exception ignore) {
                }
            }
        }
    }

    // If the String is quoted, trim the quotes.
    private static String trimquotes(String in) {
        if (in == null)
            return in;
        int length = in.length();
        if (length == 0)
            return in;
        char s = in.charAt(0);
        char e = in.charAt(length - 1);
        if (s == e && (s == '\'' || s == '"')) {
            in = in.substring(1, in.length() - 1);
        }
        return in;
    }

    // Split the supplied String into words or phrases seperated by spaces.
    // Recognises quotes around a phrase and doesn't split it.
    private static String[] split(String val) throws IllegalArgumentException {
        val = val.trim();
        if (val.length() == 0) {
            return new String[0];
        } else {
            ArrayList<String> l = new ArrayList<String>();
            int s = 0;
            int e = 0;
            boolean sq = false; // single quote
            boolean dq = false; // double quote
            char lastc = 0;
            int len = val.length();
            for (e = 0; e < len; e++) {
                char c = val.charAt(e);
                if (!dq && c == '\'' && lastc != '\\') {
                    sq = !sq;
                    if (s < 0)
                        s = e;
                } else if (!sq && c == '\"' && lastc != '\\') {
                    dq = !dq;
                    if (s < 0)
                        s = e;
                } else if (!sq && !dq) {
                    if (Character.isWhitespace(c)) {
                        if (s >= 0)
                            l.add(val.substring(s, e));
                        s = -1;
                    } else if (s < 0 && c != ' ') {
                        s = e;
                    }
                }
                lastc = c;
            }
            l.add(val.substring(s, e));
            return (String[]) l.toArray(new String[0]);
        }
    }

    // Replace junk in publicids with spaces
    private static String legal = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-'()+,./:=?;!*#@$_%";

    private String cleanPublicid(String src) {
        if (src == null)
            return null;
        int len = src.length();
        StringBuffer dst = new StringBuffer(len);
        boolean suppressSpace = true;
        for (int i = 0; i < len; i++) {
            char ch = src.charAt(i);
            if (legal.indexOf(ch) != -1) { // legal but not whitespace
                dst.append(ch);
                suppressSpace = false;
            } else if (suppressSpace) { // normalizable whitespace or junk
                ;
            } else {
                dst.append(' ');
                suppressSpace = true;
            }
        }
        return dst.toString().trim(); // trim any final junk whitespace
    }

    @Override
    public void gi(char[] buff, int offset, int length) throws SAXException {
        if (theNewElement != null)
            return;
        String name = makeName(buff, offset, length);
        if (name == null)
            return;
        ElementType type = theSchema.getElementType(name);
        if (type == null) {
            // Suppress unknown elements if ignore-bogons is on
            if (ignoreBogons)
                return;
            int bogonModel = bogonsEmpty ? Schema.M_EMPTY : Schema.M_ANY;
            int bogonMemberOf = rootBogons ? Schema.M_ANY
                    : (Schema.M_ANY & ~Schema.M_ROOT);
            theSchema.elementType(name, bogonModel, bogonMemberOf, 0);
            if (!rootBogons)
                theSchema.parent(name, theSchema.rootElementType().name());
            type = theSchema.getElementType(name);
        }

        theNewElement = new Element(type, defaultAttributes);
    }

    @Override
    public void cdsect(char[] buff, int offset, int length) throws SAXException {
        theLexicalHandler.startCDATA();
        pcdata(buff, offset, length);
        theLexicalHandler.endCDATA();
    }

    @Override
    public void pcdata(char[] buff, int offset, int length) throws SAXException {
        if (length == 0)
            return;
        boolean allWhite = true;
        for (int i = 0; i < length; i++) {
            if (!Character.isWhitespace(buff[offset + i])) {
                allWhite = false;
            }
        }
        if (allWhite && !theStack.canContain(thePCDATA)) {
            if (ignorableWhitespace) {
                theContentHandler.ignorableWhitespace(buff, offset, length);
            }
        } else {
            rectify(thePCDATA);
            theContentHandler.characters(buff, offset, length);
        }
    }

    @Override
    public void pitarget(char[] buff, int offset, int length) throws SAXException {
        if (theNewElement != null)
            return;
        thePITarget = makeName(buff, offset, length).replace(':', '_');
    }

    @Override
    public void pi(char[] buff, int offset, int length) throws SAXException {
        if (theNewElement != null || thePITarget == null)
            return;
        if ("xml".equalsIgnoreCase(thePITarget))
            return;
        // if (length > 0 && buff[length - 1] == '?')
        if (length > 0 && buff[length - 1] == '?')
            length--; // remove trailing ?
        theContentHandler.processingInstruction(thePITarget, new String(buff,
                offset, length));
        thePITarget = null;
    }

    @Override
    public void stagc(char[] buff, int offset, int length) throws SAXException {
        if (theNewElement == null)
            return;
        rectify(theNewElement);
        if (theStack.model() == Schema.M_EMPTY) {
            // Force an immediate end tag
            etag_basic(buff, offset, length);
        }
    }

    @Override
    public void stage(char[] buff, int offset, int length) throws SAXException {
        if (theNewElement == null)
            return;
        rectify(theNewElement);
        // Force an immediate end tag
        etag_basic(buff, offset, length);
    }

    @Override
    public void cmnt(char[] buff, int offset, int length) throws SAXException {
        theLexicalHandler.comment(buff, offset, length);
    }

    // Rectify the stack, pushing and popping as needed
    // so that the argument can be safely pushed
    private void rectify(Element e) throws SAXException {
        Element sp;
        while (true) {
            for (sp = theStack; sp != null; sp = sp.next()) {
                if (sp.canContain(e))
                    break;
            }
            if (sp != null)
                break;
            ElementType parentType = e.parent();
            if (parentType == null)
                break;
            Element parent = new Element(parentType, defaultAttributes);
            // parent.name());
            parent.setNext(e);
            e = parent;
        }
        if (sp == null)
            return; // don't know what to do
        while (theStack != sp) {
            if (theStack == null || theStack.next() == null
                    || theStack.next().next() == null)
                break;
            restartablyPop();
        }
        while (e != null) {
            Element nexte = e.next();
            if (!e.name().equals("<pcdata>"))
                push(e);
            e = nexte;
            restart(e);
        }
        theNewElement = null;
    }

    @Override
    public int getEntity() {
        return theEntity;
    }

    // Return the argument as a valid XML name
    // This no longer lowercases the result: we depend on Schema to
    // canonicalize case.
    private String makeName(char[] buff, int offset, int length) {
        StringBuffer dst = new StringBuffer(length + 2);
        boolean seenColon = false;
        boolean start = true;
        // String src = new String(buff, offset, length); // DEBUG
        for (; length-- > 0; offset++) {
            char ch = buff[offset];
            if (Character.isLetter(ch) || ch == '_') {
                start = false;
                dst.append(ch);
            } else if (Character.isDigit(ch) || ch == '-' || ch == '.') {
                if (start)
                    dst.append('_');
                start = false;
                dst.append(ch);
            } else if (ch == ':' && !seenColon) {
                seenColon = true;
                if (start)
                    dst.append('_');
                start = true;
                dst.append(translateColons ? '_' : ch);
            }
        }
        int dstLength = dst.length();
        if (dstLength == 0 || dst.charAt(dstLength - 1) == ':')
            dst.append('_');
        return dst.toString().intern();
    }

    // Default LexicalHandler implementation

    @Override
    public void comment(char[] ch, int start, int length) throws SAXException {
    }

    @Override
    public void endCDATA() throws SAXException {
    }

    @Override
    public void endDTD() throws SAXException {
    }

    @Override
    public void endEntity(String name) throws SAXException {
    }

    @Override
    public void startCDATA() throws SAXException {
    }

    @Override
    public void startDTD(String name, String publicid, String systemid) throws SAXException {
    }

    @Override
    public void startEntity(String name) throws SAXException {
    }

}


/*
 * 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.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.Spannable;
import android.text.Spanned;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.widget.Toast;

import com.onegravity.rteditor.LinkFragment.Link;
import com.onegravity.rteditor.LinkFragment.LinkEvent;
import com.onegravity.rteditor.RTOperationManager.TextChangeOperation;
import com.onegravity.rteditor.api.RTApi;
import com.onegravity.rteditor.api.media.RTImage;
import com.onegravity.rteditor.api.media.RTMedia;
import com.onegravity.rteditor.effects.AbsoluteSizeEffect;
import com.onegravity.rteditor.effects.AlignmentEffect;
import com.onegravity.rteditor.effects.BackgroundColorEffect;
import com.onegravity.rteditor.effects.BoldEffect;
import com.onegravity.rteditor.effects.BulletEffect;
import com.onegravity.rteditor.effects.Effect;
import com.onegravity.rteditor.effects.Effects;
import com.onegravity.rteditor.effects.ForegroundColorEffect;
import com.onegravity.rteditor.effects.ItalicEffect;
import com.onegravity.rteditor.effects.NumberEffect;
import com.onegravity.rteditor.effects.SpanCollectMode;
import com.onegravity.rteditor.effects.StrikethroughEffect;
import com.onegravity.rteditor.effects.SubscriptEffect;
import com.onegravity.rteditor.effects.SuperscriptEffect;
import com.onegravity.rteditor.effects.TypefaceEffect;
import com.onegravity.rteditor.effects.UnderlineEffect;
import com.onegravity.rteditor.fonts.RTTypeface;
import com.onegravity.rteditor.media.choose.MediaChooserActivity;
import com.onegravity.rteditor.media.choose.MediaEvent;
import com.onegravity.rteditor.spans.ImageSpan;
import com.onegravity.rteditor.spans.LinkSpan;
import com.onegravity.rteditor.spans.RTSpan;
import com.onegravity.rteditor.utils.Constants.MediaAction;
import com.onegravity.rteditor.utils.Helper;
import com.onegravity.rteditor.utils.Selection;

import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * The RTManager manages the different components:
 * the toolbar(s), the editor(s) and the Activity/Fragment(s) via the RTProxy.
 *

 * Note: the transient modifier is "misused" here to mark variables that
 * are not saved and restored in onSaveInstanceState / onCreate.
 */
public class RTManager implements RTToolbarListener, RTEditTextListener {

    /*
     * Identifies the link dialog / fragment
     */
    private static final String ID_01_LINK_FRAGMENT = "ID_01_LINK_FRAGMENT";

    /*
     * The toolbar(s) may automatically be shown or hidden when a rich text
     * editor gains or loses focus depending on the ToolbarVisibility setting.
     */
    public enum ToolbarVisibility {
        /*
         * Toolbar(s) are shown/hidden automatically depending on whether an
         * editor that uses rich text gains/loses focus.
         *
         * This is the default.
         */
        AUTOMATIC,

        /*
         * Toolbar(s) are always shown.
         */
        SHOW,

        /*
         * Toolbar(s) are never shown.
         */
        HIDE;
    }

    private ToolbarVisibility mToolbarVisibility = ToolbarVisibility.AUTOMATIC;

    /*
     * To set the visibility of the toolbar(s) we call setToolbarVisibility(boolean).
     * To change the visibility we start an animation. Before the animation ends
     * setToolbarVisibility() could have been called multiple times with different
     * visibility parameters. We need to make sure once (one of) the animation
     * ends we use the newest visibility status (that's what this variable stands for).
     *
     * We do clear the animation with each call to setToolbarVisibility but if the
     * animation has already started the onAnimationEnd is still called.
     */
    private boolean mToolbarIsVisible;

    /*
     * When an Activity is started (e.g. to pick an image),
     * we need to know which editor gets the result
     */
    private int mActiveEditor = Integer.MAX_VALUE;

    /*
     * This defines what Selection link operations are applied to
     * (inserting, editing, removing links).
     */
    private Selection mLinkSelection;

    /*
     * We need these to delay hiding the toolbar after a focus loss of an editor
     */
    transient private Handler mHandler;
    transient private boolean mIsPendingFocusLoss;
    transient private boolean mCancelPendingFocusLoss;

    /*
     * Map the registered editors by editor id (RTEditText.getId())
     */
    transient private Map<Integer, RTEditText> mEditors;

    /*
     * Map the registered toolbars by toolbar id (RTToolbar.getId())
     */
    transient private Map<Integer, RTToolbar> mToolbars;

    /*
     * That's our link to "the outside world" to perform operations that need
     * access to a Context or an Activity
     */
    transient private RTApi mRTApi;

    /*
     * The RTOperationManager is used to undo/redo operations
     */
    transient private RTOperationManager mOPManager;

    // ****************************************** Lifecycle Methods *******************************************

    /**
     * @param rtApi              The proxy to "the outside world"
     * @param savedInstanceState If the component is being re-initialized after previously
     *                           being shut down then this Bundle contains the data it most
     *                           recently supplied in onSaveInstanceState(Bundle).
     */
    public RTManager(RTApi rtApi, Bundle savedInstanceState) {
        mRTApi = rtApi;

        mHandler = new Handler();
        mEditors = new ConcurrentHashMap<Integer, RTEditText>();
        mToolbars = new ConcurrentHashMap<Integer, RTToolbar>();
        mOPManager = new RTOperationManager();

        if (savedInstanceState != null) {
            String tmp = savedInstanceState.getString("mToolbarVisibility");
            if (tmp != null) {
                mToolbarVisibility = ToolbarVisibility.valueOf(tmp);
            }
            mToolbarIsVisible = savedInstanceState.getBoolean("mToolbarIsVisible");
            mActiveEditor = savedInstanceState.getInt("mActiveEditor");
            mLinkSelection = (Selection) savedInstanceState.getSerializable("mLinkSelection");
        }

        EventBus.getDefault().register(this);
    }

    /**
     * Called to retrieve per-instance state before being killed so that the
     * state can be restored in the constructor.
     *
     * @param outState Bundle in which to place your saved state.
     */
    public void onSaveInstanceState(Bundle outState) {
        outState.putString("mToolbarVisibility", mToolbarVisibility.name());
        outState.putBoolean("mToolbarIsVisible", mToolbarIsVisible);
        outState.putInt("mActiveEditor", mActiveEditor);
        if (mLinkSelection != null) {
            outState.putSerializable("mLinkSelection", mLinkSelection);
        }
    }

    /**
     * Perform any final cleanup before the component is destroyed.
     *
     * @param isSaved True if the text is saved, False if it's dismissed. This is
     *                needed to decide whether media (images etc.) are to be
     *                deleted.
     */
    public void onDestroy(boolean isSaved) {
        EventBus.getDefault().unregister(this);

        for (RTEditText editor : mEditors.values()) {
            editor.unregister();
            editor.onDestroy(isSaved);
        }
        mEditors.clear();

        for (RTToolbar toolbar : mToolbars.values()) {
            toolbar.removeToolbarListener();
        }
        mToolbars.clear();

        mRTApi = null;
    }

    // ****************************************** Public Methods *******************************************

    /**
     * Register a rich text editor.
     *

     * Before using the editor it needs to be registered to an RTManager.
     * Using means any calls to the editor (setText will fail if the editor isn't registered)!
     * MUST be called from the ui thread.
     *
     * @param editor The rich text editor to register.
     */
    public void registerEditor(RTEditText editor, boolean useRichTextEditing) {
        mEditors.put(editor.getId(), editor);
        editor.register(this, mRTApi);
        editor.setRichTextEditing(useRichTextEditing, false);

        updateToolbarVisibility();
    }

    /**
     * Unregister a rich text editor.
     *

     * This method may be called before the component is destroyed to stop any
     * interaction with the editor. Not doing so may result in (asynchronous)
     * calls coming through when the Activity/Fragment is already stopping its
     * operation.
     *

     * Must be called from the ui thread.
     *

     * Important: calling this method is obsolete once the onDestroy(boolean) is
     * called
     *
     * @param editor The rich text editor to unregister.
     */
    public void unregisterEditor(RTEditText editor) {
        mEditors.remove(editor.getId());
        editor.unregister();

        updateToolbarVisibility();
    }

    /**
     * Register a toolbar.
     *

     * Only after doing that can it be used in conjunction with a rich text editor.
     * Must be called from the ui thread.
     *
     * @param toolbarContainer The ViewGroup containing the toolbar.
     *                         This container is used to show/hide the toolbar if needed (e.g. if the RTEditText field loses/gains focus).
     *                         We can't use the toolbar itself because there could be multiple and they could be embedded in a complex layout hierarchy.
     * @param toolbar          The toolbar to register.
     */
    public void registerToolbar(ViewGroup toolbarContainer, RTToolbar toolbar) {
        mToolbars.put(toolbar.getId(), toolbar);
        toolbar.setToolbarListener(this);
        toolbar.setToolbarContainer(toolbarContainer);

        updateToolbarVisibility();
    }

    /**
     * Unregister a toolbar.
     *

     * This method may be called before the component is destroyed to
     * stop any interaction with the toolbar. Not doing so may result
     * in (asynchronous) calls coming through when the Activity/Fragment
     * is already stopping its operation.
     *

     * Must be called from the ui thread.
     *

     * Important: calling this method is obsolete once the
     * onDestroy(boolean) is called
     *
     * @param toolbar The toolbar to unregister.
     */
    public void unregisterToolbar(RTToolbar toolbar) {
        mToolbars.remove(toolbar.getId());
        toolbar.removeToolbarListener();
        updateToolbarVisibility();
    }

    /**
     * Set the auto show/hide toolbar mode.
     */
    public void setToolbarVisibility(ToolbarVisibility toolbarVisibility) {
        if (mToolbarVisibility != toolbarVisibility) {
            mToolbarVisibility = toolbarVisibility;
            updateToolbarVisibility();
        }
    }

    private void updateToolbarVisibility() {
        boolean showToolbars = mToolbarVisibility == ToolbarVisibility.SHOW;

        if (mToolbarVisibility == ToolbarVisibility.AUTOMATIC) {
            RTEditText editor = getActiveEditor();
            showToolbars = editor != null && editor.usesRTFormatting();
        }

        for (RTToolbar toolbar : mToolbars.values()) {
            setToolbarVisibility(toolbar, showToolbars);
        }
    }

    private void setToolbarVisibility(final RTToolbar toolbar, final boolean visible) {
        mToolbarIsVisible = visible;

        final ViewGroup toolbarContainer = toolbar.getToolbarContainer();
        int visibility = View.VISIBLE;
        synchronized (toolbarContainer) {
            visibility = toolbarContainer.getVisibility();
        }

        // only change visibility if we actually have to
        if ((visibility == View.GONE && visible) || (visibility == View.VISIBLE && !visible)) {

            AlphaAnimation fadeAnimation = visible ? new AlphaAnimation(0.0f, 1.0f) : new AlphaAnimation(1.0f, 0.0f);
            fadeAnimation.setDuration(400);
            fadeAnimation.setAnimationListener(new AnimationListener() {
                @Override
                public void onAnimationStart(Animation animation) {
                }

                @Override
                public void onAnimationRepeat(Animation animation) {
                }

                @Override
                public void onAnimationEnd(Animation animation) {
                    synchronized (toolbarContainer) {
                        toolbarContainer.setVisibility(mToolbarIsVisible ? View.VISIBLE : View.GONE);
                    }
                }
            });

            toolbarContainer.startAnimation(fadeAnimation);
        } else {
            toolbarContainer.clearAnimation();
        }
    }

    // ****************************************** RTToolbarListener *******************************************

    @Override
    /* @inheritDoc */
    public <V, C extends RTSpan<V>> void onEffectSelected(Effect<V, C> effect, V value) {
        RTEditText editor = getActiveEditor();
        if (editor != null) {
            editor.applyEffect(effect, value);
        }
    }

    @Override
    /* @inheritDoc */
    public void onClearFormatting() {
        RTEditText editor = getActiveEditor();
        if (editor != null) {
            int selStartBefore = editor.getSelectionStart();
            int selEndBefore = editor.getSelectionEnd();
            Spannable oldSpannable = editor.cloneSpannable();
            for (Effect effect : Effects.FORMATTING_EFFECTS) {
                effect.clearFormattingInSelection(editor);
            }
            int selStartAfter = editor.getSelectionStart();
            int selEndAfter = editor.getSelectionEnd();
            Spannable newSpannable = editor.cloneSpannable();
            mOPManager.executed(editor, new TextChangeOperation(oldSpannable, newSpannable,
                    selStartBefore, selEndBefore,
                    selStartAfter, selEndAfter));
        }
    }

    @Override
    /* @inheritDoc */
    public void onUndo() {
        RTEditText editor = getActiveEditor();
        if (editor != null) {
            mOPManager.undo(editor);
        }
    }

    @Override
    /* @inheritDoc */
    public void onRedo() {
        RTEditText editor = getActiveEditor();
        if (editor != null) {
            mOPManager.redo(editor);
        }
    }

    @Override
    /* @inheritDoc */
    public void onCreateLink() {
        RTEditText editor = getActiveEditor();
        if (editor != null) {
            String url = null;
            String linkText = null;

            List<RTSpan<String>> links = Effects.LINK.getSpans(editor.getText(), new Selection(editor), SpanCollectMode.EXACT);
            if (links.isEmpty()) {
                // default values if no link is found at selection
                linkText = editor.getSelectedText();
                try {
                    // if this succeeds we have a valid URL and will use it for the link
                    new URL(linkText);
                    url = linkText;
                } catch (MalformedURLException ignore) {
                }
                mLinkSelection = editor.getSelection();
            } else {
                // values if a link already exists
                RTSpan<String> linkSpan = links.get(0);
                url = linkSpan.getValue();
                linkText = getLinkText(editor, linkSpan);
            }

            mRTApi.openDialogFragment(ID_01_LINK_FRAGMENT, LinkFragment.newInstance(linkText, url));
        }
    }

    @Override
    /* @inheritDoc */
    public void onPickImage() {
        onPickCaptureImage(MediaAction.PICK_PICTURE);
    }

    @Override
    /* @inheritDoc */
    public void onCaptureImage() {
        onPickCaptureImage(MediaAction.CAPTURE_PICTURE);
    }

    private void onPickCaptureImage(MediaAction mediaAction) {
        RTEditText editor = getActiveEditor();
        if (editor != null && mRTApi != null) {
            mActiveEditor = editor.getId();

            Intent intent = new Intent(RTApi.getApplicationContext(), MediaChooserActivity.class)
                    .putExtra(MediaChooserActivity.EXTRA_MEDIA_ACTION, mediaAction.name())
                    .putExtra(MediaChooserActivity.EXTRA_MEDIA_FACTORY, mRTApi);

            mRTApi.startActivityForResult(intent, mediaAction.requestCode());
        }
    }

    /* called from onEventMainThread(MediaEvent) */
    private void insertImage(final RTEditText editor, final RTImage image) {
        if (image != null && editor != null) {
            Selection selection = new Selection(editor);
            Editable str = editor.getText();

            // Unicode Character 'OBJECT REPLACEMENT CHARACTER' (U+FFFC)
            // see http://www.fileformat.info/info/unicode/char/fffc/index.htm
            str.insert(selection.start(), "\uFFFC");

            try {
                // now add the actual image and inform the RTOperationManager about the operation
                Spannable oldSpannable = editor.cloneSpannable();

                ImageSpan imageSpan = new ImageSpan(image, false);
                str.setSpan(imageSpan, selection.start(), selection.end() + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                int selStartAfter = editor.getSelectionStart();
                int selEndAfter = editor.getSelectionEnd();
                editor.onAddMedia(image);

                Spannable newSpannable = editor.cloneSpannable();

                mOPManager.executed(editor, new RTOperationManager.TextChangeOperation(oldSpannable, newSpannable,
                        selection.start(), selection.end(), selStartAfter, selEndAfter));
            } catch (OutOfMemoryError e) {
                str.delete(selection.start(), selection.end() + 1);
                mRTApi.makeText(R.string.rte_add_image_error, Toast.LENGTH_LONG).show();
            }
        }
    }

    private RTEditText getActiveEditor() {
        for (RTEditText editor : mEditors.values()) {
            if (editor.hasFocus()) {
                return editor;
            }
        }
        return null;
    }

    // ****************************************** RTEditTextListener *******************************************

    @Override
    public void onRestoredInstanceState(RTEditText editor) {
        /*
         * We need to process pending sticky MediaEvents once the editors are registered with the
         * RTManager and are fully restored.
         */
        MediaEvent event = EventBus.getDefault().getStickyEvent(MediaEvent.class);
        if (event != null) {
            onEventMainThread(event);
        }
    }

    @Override
    /* @inheritDoc */
    public void onFocusChanged(RTEditText editor, boolean focused) {
        if (editor.usesRTFormatting()) {
            synchronized (this) {
                // if a focus loss is pending then we cancel it
                if (mIsPendingFocusLoss) {
                    mCancelPendingFocusLoss = true;
                }
            }
            if (focused) {
                changeFocus();
            } else {
                mIsPendingFocusLoss = true;
                mHandler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        changeFocus();
                    }
                }, 10);
            }
        }
    }

    private void changeFocus() {
        synchronized (this) {
            if (!mCancelPendingFocusLoss) {
                updateToolbarVisibility();
            }
            mCancelPendingFocusLoss = false;
            mIsPendingFocusLoss = false;
        }
    }

    @Override
    /* @inheritDoc */
    public void onSelectionChanged(RTEditText editor, int start, int end) {
        if (editor == null) return;

        // default values
        boolean isBold = false;
        boolean isItalic = false;
        boolean isUnderLine = false;
        boolean isStrikethrough = false;
        boolean isSuperscript = false;
        boolean isSubscript = false;
        boolean isBullet = false;
        boolean isNumber = false;
        List<Alignment> alignments = null;
        List<RTTypeface> typefaces = null;
        List<Integer> sizes = null;
        List<Integer> fontColors = null;
        List<Integer> bgColors = null;

        // check if effect exists in selection
        for (Effect effect : Effects.ALL_EFFECTS) {
            if (effect.existsInSelection(editor)) {
                if (effect instanceof BoldEffect) {
                    isBold = true;
                } else if (effect instanceof ItalicEffect) {
                    isItalic = true;
                } else if (effect instanceof UnderlineEffect) {
                    isUnderLine = true;
                } else if (effect instanceof StrikethroughEffect) {
                    isStrikethrough = true;
                } else if (effect instanceof SuperscriptEffect) {
                    isSuperscript = true;
                } else if (effect instanceof SubscriptEffect) {
                    isSubscript = true;
                } else if (effect instanceof BulletEffect) {
                    isBullet = true;
                } else if (effect instanceof NumberEffect) {
                    isNumber = true;
                } else if (effect instanceof AlignmentEffect) {
                    alignments = Effects.ALIGNMENT.valuesInSelection(editor);
                } else if (effect instanceof TypefaceEffect) {
                    typefaces = Effects.TYPEFACE.valuesInSelection(editor);
                } else if (effect instanceof AbsoluteSizeEffect) {
                    sizes = Effects.FONTSIZE.valuesInSelection(editor);
                } else if (effect instanceof ForegroundColorEffect) {
                    fontColors = Effects.FONTCOLOR.valuesInSelection(editor);
                } else if (effect instanceof BackgroundColorEffect) {
                    bgColors = Effects.BGCOLOR.valuesInSelection(editor);
                }
            }
        }

        // update toolbar(s)
        for (RTToolbar toolbar : mToolbars.values()) {
            toolbar.setBold(isBold);
            toolbar.setItalic(isItalic);
            toolbar.setUnderline(isUnderLine);
            toolbar.setStrikethrough(isStrikethrough);
            toolbar.setSuperscript(isSuperscript);
            toolbar.setSubscript(isSubscript);
            toolbar.setBullet(isBullet);
            toolbar.setNumber(isNumber);

            // alignment (left, center, right)
            if (alignments != null && alignments.size() == 1) {
                toolbar.setAlignment(alignments.get(0));
            } else {
                boolean isRTL = Helper.isRTL(editor.getText(), start, end);
                toolbar.setAlignment(isRTL ? Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_NORMAL);
            }

            // fonts
            if (typefaces != null && typefaces.size() == 1) {
                toolbar.setFont(typefaces.get(0));
            }
            else {
                toolbar.setFont(null);
            }

            // text size
            if (sizes == null) {
                toolbar.setFontSize(Math.round(editor.getTextSize()));
            } else if (sizes.size() == 1) {
                toolbar.setFontSize(sizes.get(0));
            } else {
                toolbar.setFontSize(-1);
            }

            // font color
            if (fontColors != null && fontColors.size() == 1) {
                toolbar.setFontColor(fontColors.get(0));
            } else {
                toolbar.removeFontColor();
            }

            // background color
            if (bgColors != null && bgColors.size() == 1) {
                toolbar.setBGColor(bgColors.get(0));
            } else {
                toolbar.removeBGColor();
            }
        }
    }

    @Override
    /* @inheritDoc */
    public void onTextChanged(RTEditText editor, Spannable before, Spannable after,
                              int selStartBefore, int selEndBefore, int selStartAfter, int selEndAfter) {
        TextChangeOperation op = new TextChangeOperation(before, after,
                selStartBefore, selEndBefore,
                selStartAfter, selEndAfter);
        mOPManager.executed(editor, op);
    }

    @Override
    /* @inheritDoc */
    public void onClick(RTEditText editor, LinkSpan span) {
        if (editor != null) {
            String linkText = getLinkText(editor, span);
            mRTApi.openDialogFragment(ID_01_LINK_FRAGMENT, LinkFragment.newInstance(linkText, span.getURL()));
        }
    }

    private String getLinkText(RTEditText editor, RTSpan<String> span) {
        Spannable text = editor.getText();
        final int spanStart = text.getSpanStart(span);
        final int spanEnd = text.getSpanEnd(span);
        String linkText = null;
        if (spanStart >= 0 && spanEnd >= 0 && spanEnd <= text.length()) {
            linkText = text.subSequence(spanStart, spanEnd).toString();
            mLinkSelection = new Selection(spanStart, spanEnd);
        } else {
            mLinkSelection = editor.getSelection();
        }
        return linkText;
    }

    /**
     * Media file was picked -> process the result.
     */
    @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
    public void onEventMainThread(MediaEvent event) {
        RTEditText editor = mEditors.get(mActiveEditor);
        RTMedia media = event.getMedia();
        if (editor != null && media instanceof RTImage) {
            insertImage(editor, (RTImage) media);
            EventBus.getDefault().removeStickyEvent(event);
            mActiveEditor = Integer.MAX_VALUE;
        }
    }

    /**
     * LinkFragment has closed -> process the result.
     */
    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventMainThread(LinkEvent event) {
        final String fragmentTag = event.getFragmentTag();
        mRTApi.removeFragment(fragmentTag);

        if (!event.wasCancelled() && ID_01_LINK_FRAGMENT.equals(fragmentTag)) {

            RTEditText editor = getActiveEditor();
            if (editor != null) {

                Link link = event.getLink();
                String url = null;
                if (link != null && link.isValid()) {

                    // the mLinkSelection.end() <= editor.length() check is necessary since
                    // the editor text can change when the link fragment is open
                    Selection selection = mLinkSelection != null && mLinkSelection.end() <= editor.length() ? mLinkSelection : new Selection(editor);

                    String linkText = link.getLinkText();

                    // if no text is selected this inserts the entered link text
                    // if text is selected we replace it by the link text
                    Editable str = editor.getText();
                    str.replace(selection.start(), selection.end(), linkText);
                    editor.setSelection(selection.start(), selection.start() + linkText.length());

                    url = link.getUrl();

                }
                editor.applyEffect(Effects.LINK, url);    // if url == null -> remove the link

            }

        }
    }

    @Override
    public void onRichTextEditingChanged(RTEditText editor, boolean useRichText) {
        updateToolbarVisibility();
    }

}


/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.onegravity.rteditor.media.crop;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.widget.ImageView;

abstract class ImageViewTouchBase extends ImageView {

    @SuppressWarnings("unused")
    private static final String TAG = "ImageViewTouchBase";

    // This is the base transformation which is used to show the image
    // initially. The current computation for this shows the image in
    // it's entirety, letterboxing as needed. One could choose to
    // show the image as cropped instead.
    //
    // This matrix is recomputed when we go from the thumbnail image to
    // the full size image.
    protected Matrix mBaseMatrix = new Matrix();

    // This is the supplementary transformation which reflects what
    // the user has done in terms of zooming and panning.
    //
    // This matrix remains the same when we go from the thumbnail image
    // to the full size image.
    protected Matrix mSuppMatrix = new Matrix();

    // This is the final matrix which is computed as the concatentation
    // of the base matrix and the supplementary matrix.
    private final Matrix mDisplayMatrix = new Matrix();

    // Temporary buffer used for getting the values out of a matrix.
    private final float[] mMatrixValues = new float[9];

    // The current bitmap being displayed.
    final protected RotateBitmap mBitmapDisplayed = new RotateBitmap(null);

    int mThisWidth = -1, mThisHeight = -1;

    float mMaxZoom;

    int mLeft;

    int mRight;

    int mTop;

    int mBottom;

    // ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished
    // its use of that Bitmap.
    public interface Recycler {
        public void recycle(Bitmap b);
    }

    public void setRecycler(Recycler r) {
        mRecycler = r;
    }

    private Recycler mRecycler;

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        mLeft = left;
        mRight = right;
        mTop = top;
        mBottom = bottom;
        mThisWidth = right - left;
        mThisHeight = bottom - top;
        Runnable r = mOnLayoutRunnable;
        if (r != null) {
            mOnLayoutRunnable = null;
            r.run();
        }
        if (mBitmapDisplayed.getBitmap() != null) {
            getProperBaseMatrix(mBitmapDisplayed, mBaseMatrix);
            setImageMatrix(getImageViewMatrix());
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && getScale() > 1.0f) {
            // If we're zoomed in, pressing Back jumps out to show the entire
            // image, otherwise Back returns the user to the gallery.
            zoomTo(1.0f);
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    protected Handler mHandler = new Handler();

    @Override
    public void setImageBitmap(Bitmap bitmap) {
        setImageBitmap(bitmap, 0);
    }

    private void setImageBitmap(Bitmap bitmap, int rotation) {
        super.setImageBitmap(bitmap);
        Drawable d = getDrawable();
        if (d != null) {
            d.setDither(true);
        }

        Bitmap old = mBitmapDisplayed.getBitmap();
        mBitmapDisplayed.setBitmap(bitmap);
        mBitmapDisplayed.setRotation(rotation);

        if (old != null && old != bitmap && mRecycler != null) {
            mRecycler.recycle(old);
        }
    }

    public void clear() {
        setImageBitmapResetBase(null, true);
    }

    private Runnable mOnLayoutRunnable = null;

    // This function changes bitmap, reset base matrix according to the size
    // of the bitmap, and optionally reset the supplementary matrix.
    public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) {
        setImageRotateBitmapResetBase(new RotateBitmap(bitmap), resetSupp);
    }

    public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, final boolean resetSupp) {

        final int viewWidth = getWidth();

        if (viewWidth <= 0) {
            mOnLayoutRunnable = new Runnable() {
                public void run() {

                    setImageRotateBitmapResetBase(bitmap, resetSupp);
                }
            };
            return;
        }

        if (bitmap.getBitmap() != null) {
            getProperBaseMatrix(bitmap, mBaseMatrix);
            setImageBitmap(bitmap.getBitmap(), bitmap.getRotation());
        } else {
            mBaseMatrix.reset();
            setImageBitmap(null);
        }

        if (resetSupp) {
            mSuppMatrix.reset();
        }
        setImageMatrix(getImageViewMatrix());
        mMaxZoom = maxZoom();
    }

    // Center as much as possible in one or both axis. Centering is
    // defined as follows: if the image is scaled down below the
    // view's dimensions then center it (literally). If the image
    // is scaled larger than the view and is translated out of view
    // then translate it back into view (i.e. eliminate black bars).
    protected void center(boolean horizontal, boolean vertical) {
        if (mBitmapDisplayed.getBitmap() == null) {
            return;
        }

        Matrix m = getImageViewMatrix();

        RectF rect = new RectF(0, 0, mBitmapDisplayed.getBitmap().getWidth(),
                mBitmapDisplayed.getBitmap().getHeight());

        m.mapRect(rect);

        float height = rect.height();
        float width = rect.width();

        float deltaX = 0, deltaY = 0;

        if (vertical) {
            int viewHeight = getHeight();
            if (height < viewHeight) {
                deltaY = (viewHeight - height) / 2 - rect.top;
            } else if (rect.top > 0) {
                deltaY = -rect.top;
            } else if (rect.bottom < viewHeight) {
                deltaY = getHeight() - rect.bottom;
            }
        }

        if (horizontal) {
            int viewWidth = getWidth();
            if (width < viewWidth) {
                deltaX = (viewWidth - width) / 2 - rect.left;
            } else if (rect.left > 0) {
                deltaX = -rect.left;
            } else if (rect.right < viewWidth) {
                deltaX = viewWidth - rect.right;
            }
        }

        postTranslate(deltaX, deltaY);
        setImageMatrix(getImageViewMatrix());
    }

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

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

    private void init() {
        setScaleType(ScaleType.MATRIX);
    }

    protected float getValue(Matrix matrix, int whichValue) {
        matrix.getValues(mMatrixValues);
        return mMatrixValues[whichValue];
    }

    // Get the scale factor out of the matrix.
    protected float getScale(Matrix matrix) {
        return getValue(matrix, Matrix.MSCALE_X);
    }

    protected float getScale() {
        return getScale(mSuppMatrix);
    }

    // Setup the base matrix so that the image is centered and scaled properly.
    private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix) {
        float viewWidth = getWidth();
        float viewHeight = getHeight();

        float w = bitmap.getWidth();
        float h = bitmap.getHeight();
        matrix.reset();

        // We limit up-scaling to 2x otherwise the result may look bad if it's
        // a small icon.
        float widthScale = Math.min(viewWidth / w, 2.0f);
        float heightScale = Math.min(viewHeight / h, 2.0f);
        float scale = Math.min(widthScale, heightScale);

        matrix.postConcat(bitmap.getRotateMatrix());
        matrix.postScale(scale, scale);

        matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F);
    }

    // Combine the base matrix and the supp matrix to make the final matrix.
    protected Matrix getImageViewMatrix() {
        // The final matrix is computed as the concatentation of the base matrix
        // and the supplementary matrix.
        mDisplayMatrix.set(mBaseMatrix);
        mDisplayMatrix.postConcat(mSuppMatrix);
        return mDisplayMatrix;
    }

    static final float SCALE_RATE = 1.25F;

    // Sets the maximum zoom, which is a scale relative to the base matrix. It
    // is calculated to show the image at 400% zoom regardless of screen or
    // image orientation. If in the future we decode the full 3 megapixel image,
    // rather than the current 1024x768, this should be changed down to 200%.
    protected float maxZoom() {
        if (mBitmapDisplayed.getBitmap() == null) {
            return 1F;
        }

        float fw = (float) mBitmapDisplayed.getWidth() / (float) mThisWidth;
        float fh = (float) mBitmapDisplayed.getHeight() / (float) mThisHeight;
        float max = Math.max(fw, fh) * 4;
        return max;
    }

    protected void zoomTo(float scale, float centerX, float centerY) {
        if (scale > mMaxZoom) {
            scale = mMaxZoom;
        }

        float oldScale = getScale();
        float deltaScale = scale / oldScale;

        mSuppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
        setImageMatrix(getImageViewMatrix());
        center(true, true);
    }

    protected void zoomTo(final float scale, final float centerX, final float centerY, final float durationMs) {

        final float incrementPerMs = (scale - getScale()) / durationMs;
        final float oldScale = getScale();
        final long startTime = System.currentTimeMillis();

        mHandler.post(new Runnable() {
            public void run() {

                long now = System.currentTimeMillis();
                float currentMs = Math.min(durationMs, now - startTime);
                float target = oldScale + (incrementPerMs * currentMs);
                zoomTo(target, centerX, centerY);

                if (currentMs < durationMs) {
                    mHandler.post(this);
                }
            }
        });
    }

    protected void zoomTo(float scale) {
        float cx = getWidth() / 2F;
        float cy = getHeight() / 2F;

        zoomTo(scale, cx, cy);
    }

    protected void zoomIn() {
        zoomIn(SCALE_RATE);
    }

    protected void zoomOut() {
        zoomOut(SCALE_RATE);
    }

    protected void zoomIn(float rate) {
        if (getScale() >= mMaxZoom) {
            return; // Don't let the user zoom into the molecular level.
        }
        if (mBitmapDisplayed.getBitmap() == null) {
            return;
        }

        float cx = getWidth() / 2F;
        float cy = getHeight() / 2F;

        mSuppMatrix.postScale(rate, rate, cx, cy);
        setImageMatrix(getImageViewMatrix());
    }

    protected void zoomOut(float rate) {
        if (mBitmapDisplayed.getBitmap() == null) {
            return;
        }

        float cx = getWidth() / 2F;
        float cy = getHeight() / 2F;

        // Zoom out to at most 1x.
        Matrix tmp = new Matrix(mSuppMatrix);
        tmp.postScale(1F / rate, 1F / rate, cx, cy);

        if (getScale(tmp) < 1F) {
            mSuppMatrix.setScale(1F, 1F, cx, cy);
        } else {
            mSuppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
        }
        setImageMatrix(getImageViewMatrix());
        center(true, true);
    }

    protected void postTranslate(float dx, float dy) {
        mSuppMatrix.postTranslate(dx, dy);
    }

    protected void panBy(float dx, float dy) {
        postTranslate(dx, dy);
        setImageMatrix(getImageViewMatrix());
    }
}


/*
 * 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.effects;

import android.text.Spannable;

import com.onegravity.rteditor.RTEditText;
import com.onegravity.rteditor.spans.RTSpan;
import com.onegravity.rteditor.utils.Selection;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;

/**
 * Base class for all effects.
 * An "effect" is a particular type of styling to apply to the selected text in a rich text editor.
 * Most of them are wrappers around the corresponding CharacterStyle (Bold, Italic, font size etc.)
 * or ParagraphStyle classes (e.g. BulletSpan).
 *
 * @param <V> is the sort of configuration information that the effect needs.
 *           Many will be Effect<Boolean>, meaning the effect is a toggle (on or off), such as bold.
 *
 * @param <C> is the RTSpan<V> used by the Effect (e.g. BoldEffect uses BoldSpan)
 */
abstract public class Effect<V, C extends RTSpan<V>> {

    private SpanCollector<V> mSpanCollector;

    /**
     * Check whether the effect exists in the currently selected text of the active RTEditText.
     *
     * @param editor The RTEditText we want the check.
     *
     * @return True if the effect exists in the current selection, False otherwise.
     */
    final public boolean existsInSelection(RTEditText editor) {
        Selection selection = getSelection(editor);
        List<RTSpan<V>> spans = getSpans(editor.getText(), selection, SpanCollectMode.SPAN_FLAGS);

        return ! spans.isEmpty();
    }

    /**
     * Returns the value(s) of this effect in the currently selected text of the active RTEditText.
     *
     * @return The returned list, must NEVER be null.
     */
    final public List<V> valuesInSelection(RTEditText editor) {
        List<V> result = new ArrayList<V>();

        Selection selection = getSelection(editor);
        List<RTSpan<V>> spans = getSpans(editor.getText(), selection, SpanCollectMode.SPAN_FLAGS);
        for (RTSpan<V> span : spans) {
            result.add( span.getValue() );
        }

        return result;
    }

    /**
     * Remove all effects of this type from the currently selected text of the active RTEditText.
     * If the selection is empty (cursor), formatting for the whole text is removed.
     */
    final public void clearFormattingInSelection(RTEditText editor) {
        Spannable text = editor.getText();

        // if no selection --> select the whole text
        // otherwise use the getSelection method (implented by sub classes)
        Selection selection = new Selection(editor);
        selection = selection.isEmpty() ? new Selection(0, text.length()) : getSelection(editor);

        List<RTSpan<V>> spans = getSpans(text, selection, SpanCollectMode.EXACT);
        for (Object span : spans) {
            editor.getText().removeSpan(span);
        }
    }

    /**
     * Equivalent to the Spanned.getSpans(int, int, Class<T>) method.
     * Return the markup objects (spans) attached to the specified slice of a Spannable.
     * The type of the spans is defined in the SpanCollector.
     *
     * @param str The Spannable to search for spans.
     * @param selection The selection within the Spannable to search for spans.
     * @param mode details see SpanCollectMode.
     *
     * @return the list of spans in this Spannable/Selection, never Null
     */
    final public List<RTSpan<V>> getSpans(Spannable str, Selection selection, SpanCollectMode mode) {
        if (mSpanCollector == null) {
            // lazy initialize the SpanCollector
            Type[] types = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments();
            Class<? extends RTSpan<V>> spanClazz = (Class<? extends RTSpan<V>>) types[types.length - 1];
            mSpanCollector = newSpanCollector(spanClazz);
        }

        return mSpanCollector.getSpans(str, selection, mode);
    }

    /**
     * @return a new SpanCollector for this effect
     */
    abstract protected SpanCollector<V> newSpanCollector(Class<? extends RTSpan<V>> spanClazz);

    /**
     * @return the Selection for the specified RTEditText.
     */
    abstract protected Selection getSelection(RTEditText editor);

    /**
     * Apply this effect to the selection.
     * If value is Null then the effect will be removed from the current selection.
     *
     * @param editor The editor to apply the effect to (current selection)
     * @param value The value to apply (depends on the Effect)
     */
    abstract public void applyToSelection(RTEditText editor, V value);

}


/*
 * 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.effects;

import android.text.Spannable;

import com.onegravity.rteditor.RTEditText;
import com.onegravity.rteditor.spans.RTSpan;
import com.onegravity.rteditor.utils.Paragraph;
import com.onegravity.rteditor.utils.Selection;

import java.util.List;

/**
 * ParagraphEffect are always applied to whole paragraphs, like bullet points or alignment.
 *
 * If we apply ParagraphEffects we need to call the cleanupParagraphs() afterwards!
 */
abstract class ParagraphEffect<V, C extends RTSpan<V>> extends Effect<V, C> {

    @Override
    final protected SpanCollector<V> newSpanCollector(Class<? extends RTSpan<V>> spanClazz) {
        return new ParagraphSpanCollector<V>(spanClazz);
    }

    /**
     * @return the start and end of the paragraph(s) encompassing the current selection because
     *         ParagraphEffects always operate on whole paragraphs.
     */
    @Override
    final protected Selection getSelection(RTEditText editor) {
        return editor.getParagraphsInSelection();
    }

    /**
     * Make sure applyToSelection works on whole paragraphs and call
     * Effects.cleanupParagraphs(RTEditText) afterwards.
     */
    @Override
    public final void applyToSelection(RTEditText editor, V value) {
        Selection selection = getSelection(editor);
        applyToSelection(editor, selection, value);
        Effects.cleanupParagraphs(editor, this);
    }

    /**
     * Apply this effect to the selection.
     *
     * Effects.cleanupParagraphs(RTEditText) calls this method using:
     * applyToSelection(RTEditText, null, null).
     * The result must be that the no effects are added or removed but every effect is applied to
     * a whole paragraph and opposing effects aren't applied to the same paragraph (bullet points
     * and numbering can't be applied to the same paragraph).
     *
     * @param editor The editor to apply the effect to (current selection)
     * @param selectedParagraphs Apply the effect to the selected paragraphs
     * @param value The value to apply (depends on the Effect)
     */
    public abstract void applyToSelection(RTEditText editor, Selection selectedParagraphs, V value);

    /**
     * Find spans within that paragraph and add them to the ParagraphSpanProcessor to be removed
     * once the ParagraphSpanProcessor processes its spans.
     */
    protected void findSpans2Remove(Spannable str, Paragraph paragraph,
                                    ParagraphSpanProcessor<V> spanProcessor) {
        List<RTSpan<V>> spans = getSpans(str, paragraph, SpanCollectMode.EXACT);
        spanProcessor.removeSpans(spans, paragraph);
    }

}