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

import android.content.Context;
import android.content.res.Configuration;
import android.net.Uri;
import android.util.DisplayMetrics;
import android.view.Display;
import android.view.WindowManager;

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

import java.io.Closeable;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.text.Bidi;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * Miscellaneous helper methods
 */
public abstract class Helper {

    private static float sDensity = Float.MAX_VALUE;
    private static float sDensity4Fonts = Float.MAX_VALUE;

    private static final int LEADING_MARGIN = 28;
    private static int sLeadingMarging = -1;

    public static void closeQuietly(Closeable closeable) {
        IOUtils.closeQuietly(closeable);
    }

    public static float getDisplayDensity() {
        synchronized (Helper.class) {
            if (sDensity == Float.MAX_VALUE) {
                sDensity = getDisplayMetrics().density;
            }
            return sDensity;
        }
    }

    /**
     * Convert absolute pixels to scale dependent pixels.
     * This scales the size by scale dependent screen density (accessibility setting) and
     * the global display setting for message composition fields
     */
    public static int convertPxToSp(int pxSize) {
        return Math.round((float) pxSize * getDisplayDensity4Fonts());
    }

    /**
     * Convert scale dependent pixels to absolute pixels.
     * This scales the size by scale dependent screen density (accessibility setting) and
     * the global display setting for message composition fields
     */
    public static int convertSpToPx(int spSize) {
        return Math.round((float) spSize / getDisplayDensity4Fonts());
    }

    private static DisplayMetrics getDisplayMetrics() {
        Display display = ((WindowManager) RTApi.getApplicationContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
        DisplayMetrics metrics = new DisplayMetrics();
        display.getMetrics(metrics);
        return metrics;
    }

    private static float getDisplayDensity4Fonts() {
        synchronized (Helper.class) {
            if (sDensity4Fonts == Float.MAX_VALUE) {
                sDensity4Fonts = getDisplayMetrics().density * getFontScale();
            }
            return sDensity4Fonts;
        }
    }

    private static float getFontScale() {
        Configuration config = RTApi.getApplicationContext().getResources().getConfiguration();
        return config.fontScale;
    }

    public static int getLeadingMarging() {
        if (sLeadingMarging == -1) {
            float density = Helper.getDisplayDensity();
            sLeadingMarging = Math.round(LEADING_MARGIN * density);
        }
        return sLeadingMarging;
    }

    /**
     * This method encodes the query part of an url
     * @param url an url (e.g. http://www.1gravity.com?query=üö)
     * @return The url with an encoded query, e.g. http://www.1gravity.com?query%3D%C3%BC%C3%B6
     */
    public static String encodeUrl(String url) {
        Uri uri = Uri.parse(url);

        try {
            Map<String, List<String>> splitQuery = splitQuery(uri);
            StringBuilder encodedQuery = new StringBuilder();
            for (String key : splitQuery.keySet()) {
                for (String value : splitQuery.get(key)) {
                    if (encodedQuery.length() > 0) {
                        encodedQuery.append("&");
                    }
                    encodedQuery.append(key + "=" + URLEncoder.encode(value, "UTF-8"));
                }
            }
            String queryString = encodedQuery != null && encodedQuery.length() > 0 ? "?" + encodedQuery : "";

            URI baseUri = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), null, uri.getFragment());
            return baseUri + queryString;
        }
        catch (UnsupportedEncodingException ignore) {}
        catch (URISyntaxException ignore) {}

        return uri.toString();
    }

    /**
     * This method decodes an url with encoded query string
     * @param url an url with encoded query string (e.g. http://www.1gravity.com?query%C3%BC%C3%B6)
     * @return The decoded url, e.g. http://www.1gravity.com?query=üö
     */
    public static String decodeQuery(String url) {
        try {
            return URLDecoder.decode(url, "UTF-8");
        }
        catch (UnsupportedEncodingException ignore) {}

        return url;
    }

    /**
     * Splits the query parameters into key value pairs.
     * See: http://stackoverflow.com/a/13592567/534471.
     */
    private static Map<String, List<String>> splitQuery(Uri uri) throws UnsupportedEncodingException {
        final Map<String, List<String>> query_pairs = new LinkedHashMap<>();
        String query = uri.getQuery();
        if (query == null) return query_pairs;

        final String[] pairs = query.split("&");
        for (String pair : pairs) {
            final int idx = pair.indexOf("=");
            final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), "UTF-8") : pair;
            if (!query_pairs.containsKey(key)) {
                query_pairs.put(key, new LinkedList<String>());
            }
            final String value = idx > 0 && pair.length() > idx + 1 ? URLDecoder.decode(pair.substring(idx + 1), "UTF-8") : "";
            query_pairs.get(key).add(value);
        }
        return query_pairs;
    }

    /**
     * This method determines if the direction of a substring is right-to-left.
     * If the string is empty that determination is based on the default system language
     * Locale.getDefault().
     * The method can handle invalid substring definitions (start > end etc.), in which case the
     * method returns False.
     *
     * @return True if the text direction is right-to-left, false otherwise.
     */
    public static boolean isRTL(CharSequence s, int start, int end) {
        if (s == null || s.length() == 0) {
            // empty string --> determine the direction from the default language
            return isRTL(Locale.getDefault());
        }

        if (start == end) {
            // if no character is selected we need to expand the selection
            start = Math.max(0, --start);
            if (start == end) {
                end = Math.min(s.length(), ++end);
            }
        }

        try {
            Bidi bidi = new Bidi(s.subSequence(start, end).toString(), Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT);
            return ! bidi.baseIsLeftToRight();
        }
        catch (IndexOutOfBoundsException e) {
            return false;
        }
    }

    private static boolean isRTL(Locale locale) {
        int directionality = Character.getDirectionality(locale.getDisplayName().charAt(0));
        return directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT ||
                directionality == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC;
    }

}


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

import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.widget.Toast;

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

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;

public class FileHelper {

    private static final String[][] PICK_DIRECTORY_INTENTS = {
            {"org.openintents.action.PICK_DIRECTORY", "file://"},        // OI File Manager (maybe others)
            {"com.estrongs.action.PICK_DIRECTORY", "file://"},            // ES File Explorer
            {Intent.ACTION_PICK, "folder://"},                            // Blackmoon File Browser (maybe others)
            {"com.androidworkz.action.PICK_DIRECTORY", "file://"},
    };

    /**
     * Tries to open a known file browsers to pick a directory.
     *
     * @return True if a filebrowser has been found (the result will be in the onActivityResult), False otherwise
     */
    public static boolean pickDirectory(Activity activity, File startPath, int requestCode) {
        PackageManager packageMgr = activity.getPackageManager();
        for (String[] intent : PICK_DIRECTORY_INTENTS) {
            String intentAction = intent[0];
            String uriPrefix = intent[1];
            Intent startIntent = new Intent(intentAction)
                    .putExtra("org.openintents.extra.TITLE", activity.getString(R.string.save_as))
                    .setData(Uri.parse(uriPrefix + startPath.getPath()));

            try {
                if (startIntent.resolveActivity(packageMgr) != null) {
                    activity.startActivityForResult(startIntent, requestCode);
                    return true;
                }
            } catch (ActivityNotFoundException e) {
                showNoFilePickerError(activity, e);
            }
        }

        return false;
    }

    public static boolean pickFile(Activity activity, int requestCode) {
        try {
            Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
            intent.setType("file/*");
            activity.startActivityForResult(intent, requestCode);
            return true;
        }
        catch (ActivityNotFoundException e) {
            showNoFilePickerError(activity, e);
        }
        return false;
    }

    private static void showNoFilePickerError(Context context, Exception e) {
        String msg = context.getString(R.string.no_file_picker, e.getMessage());
        Toast.makeText(context, msg , Toast.LENGTH_LONG).show();
    }

    public static String save(Context context, File outFile, String html) {
        Reader in = null;
        Writer out = null;

        try {
            in = new StringReader(html);
            out = new FileWriter(outFile);

            IOUtils.copy(in, out);

            return outFile.getAbsolutePath();
        } catch (IOException ioe) {
            String toastMsg = context.getString(R.string.save_as_failure, ioe.getMessage());
            Toast.makeText(context, toastMsg, Toast.LENGTH_LONG).show();
        } finally {
            Helper.closeQuietly(in);
            Helper.closeQuietly(out);
        }

        return null;
    }

    public static String load(Context context, String filePath) {
        File inFile = new File(filePath);
        Reader in = null;
        Writer out = null;

        try {
            in = new FileReader(inFile);
            out = new StringWriter();

            IOUtils.copy(in, out);

            return out.toString();
        } catch (IOException ioe) {
            String toastMsg = context.getString(R.string.load_failure_2, ioe.getMessage());
            Toast.makeText(context, toastMsg, Toast.LENGTH_LONG).show();
        } finally {
            Helper.closeQuietly(in);
            Helper.closeQuietly(out);
        }

        return null;
    }

}


/*
 * 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.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.media.FaceDetector;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.Surface;
import android.view.View;

import com.onegravity.rteditor.R;
import com.onegravity.rteditor.media.MediaUtils;
import com.onegravity.rteditor.media.MonitoredActivity;
import com.onegravity.rteditor.utils.Helper;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * The activity can crop specific region of interest from an image.
 */
public class CropImageActivity extends MonitoredActivity {

    private final float IMAGE_MAX_SIZE = 1024f;

    public static final String IMAGE_SOURCE_FILE = "image-source-file";
    public static final String IMAGE_DESTINATION_FILE = "image-dest-file";
    public static final String SCALE = "scale";
    public static final String ORIENTATION_IN_DEGREES = "orientation_in_degrees";
    public static final String ASPECT_X = "aspectX";
    public static final String ASPECT_Y = "aspectY";
    public static final String OUTPUT_X = "outputX";
    public static final String OUTPUT_Y = "outputY";
    public static final String SCALE_UP_IF_NEEDED = "scaleUpIfNeeded";
    public static final String CIRCLE_CROP = "circleCrop";
    public static final String RETURN_DATA = "return-data";
    public static final String RETURN_DATA_AS_BITMAP = "data";
    public static final String ACTION_INLINE_DATA = "inline-data";

    // These are various options can be specified in the intent.
    private Bitmap.CompressFormat mOutputFormat = Bitmap.CompressFormat.JPEG;
    private Uri mSaveUri = null;
    private boolean mDoFaceDetection = false;
    private boolean mCircleCrop = false;

    private int mAspectX;
    private int mAspectY;
    private int mOutputX;
    private int mOutputY;
    private boolean mScale;
    private CropImageView mImageView;
    private Bitmap mBitmap;
    private String mImageSource;
    private String mImageDest;

    boolean mWaitingToPick; // Whether we are wait the user to pick a face.
    boolean mSaving; // Whether the "save" button is already clicked.
    HighlightView mCrop;

    // These options specify the output image size and whether we should
    // scale the output to fit it (or just crop it).
    private boolean mScaleUp = true;

    private final BitmapManager.ThreadSet mDecodingThreads = new BitmapManager.ThreadSet();

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.rte_crop_image);
        mImageView = (CropImageView) findViewById(R.id.image);

        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {

            if (extras.getString(CIRCLE_CROP) != null) {
                mImageView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
                mCircleCrop = true;
                mAspectX = 1;
                mAspectY = 1;
            }

            mImageSource = extras.getString(IMAGE_SOURCE_FILE);
            mBitmap = getBitmap(mImageSource);

            mImageDest = extras.getString(IMAGE_DESTINATION_FILE);
            if (mImageDest == null) {
                mImageDest = mImageSource;
            }
            mSaveUri = Uri.fromFile(new File(mImageDest));

            if (extras.containsKey(ASPECT_X)
                    && extras.get(ASPECT_X) instanceof Integer) {
                mAspectX = extras.getInt(ASPECT_X);
            } else {
                throw new IllegalArgumentException("aspect_x must be integer");
            }
            if (extras.containsKey(ASPECT_Y)
                    && extras.get(ASPECT_Y) instanceof Integer) {
                mAspectY = extras.getInt(ASPECT_Y);
            } else {
                throw new IllegalArgumentException("aspect_y must be integer");
            }
            mOutputX = extras.getInt(OUTPUT_X);
            mOutputY = extras.getInt(OUTPUT_Y);
            mScale = extras.getBoolean(SCALE, true);
            mScaleUp = extras.getBoolean(SCALE_UP_IF_NEEDED, true);
        }

        if (mBitmap == null) {
            finish();
            return;
        }

        startFaceDetection();
    }

    // ****************************************** ActionBar / Option Menu *******************************************

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        super.onCreateOptionsMenu(menu);
        getMenuInflater().inflate(R.menu.rte_crop_image, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int itemId = item.getItemId();
        if (itemId == R.id.save) {
            try {
                onSave();
            } catch (Exception e) {
                Log.e(getClass().getSimpleName(), e.getMessage(), e);
                finish();
            }
            return true;
        } else if (itemId == R.id.cancel) {
            setResult(RESULT_CANCELED);
            finish();
            return true;
        } else if (itemId == R.id.rotate_left) {
            mBitmap = rotateImage(mBitmap, -90);
            RotateBitmap rotateBitmap = new RotateBitmap(mBitmap);
            mImageView.setImageRotateBitmapResetBase(rotateBitmap, true);
            mRunFaceDetection.run();
            return true;
        } else if (itemId == R.id.rotate_right) {
            mBitmap = rotateImage(mBitmap, 90);
            RotateBitmap rotateBitmap = new RotateBitmap(mBitmap);
            mImageView.setImageRotateBitmapResetBase(rotateBitmap, true);
            mRunFaceDetection.run();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    // ****************************************** Private Methods *******************************************

    private Bitmap getBitmap(String path) {
        Uri uri = MediaUtils.createFileUri(path);
        InputStream in = null;
        try {
            in = getContentResolver().openInputStream(uri);

            // Decode image size
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeStream(in, null, options);
            Helper.closeQuietly(in);

            // compute scale factor to ensure that the image is smaller than
            // IMAGE_MAX_SIZE
            float maxSize = Math.max(options.outWidth, options.outHeight);
            float scale = maxSize > IMAGE_MAX_SIZE ? maxSize / IMAGE_MAX_SIZE
                    : 1.0f;

            while ((maxSize / scale) > 8) {
                try {
                    return getBitmap(in, uri, scale);
                } catch (Throwable e) {
                    Log.w(getClass().getSimpleName(),
                            "bitmap could not be created (probably out of memory), decreasing size and retrying");
                    scale *= 2f;
                }
            }
        } catch (IOException e) {
            Log.e(getClass().getSimpleName(), "file " + path + " not found");
        } catch (Exception e) {
            Log.e(getClass().getSimpleName(), "error while opening image", e);
        } finally {
            Helper.closeQuietly(in);
        }
        return null;
    }

    private Bitmap getBitmap(InputStream in, Uri uri, float scale)
            throws Throwable {
        // decode image
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
        options.inSampleSize = Math.round(scale);
        in = getContentResolver().openInputStream(uri);
        return BitmapFactory.decodeStream(in, null, options);
    }

    private void startFaceDetection() {

        if (isFinishing()) {
            return;
        }

        mImageView.setImageBitmapResetBase(mBitmap, true);

        startBackgroundJob(R.string.rte_processing_image, new Runnable() {
            public void run() {
                final Bitmap b = mBitmap;
                if (b != mBitmap && b != null) {
                    mImageView.setImageBitmapResetBase(b, true);
                    mBitmap.recycle();
                    mBitmap = b;
                }
                if (mImageView.getScale() == 1F) {
                    mImageView.center(true, true);
                }
                mRunFaceDetection.run();
            }
        });
    }

    private void onSave() throws Exception {
        // TODO this code needs to change to use the decode/crop/encode single
        // step api so that we don't require that the whole (possibly large)
        // bitmap doesn't have to be read into memory
        if (mSaving)
            return;

        if (mCrop == null) {

            return;
        }

        mSaving = true;

        Rect r = mCrop.getCropRect();

        int width = r.width();
        int height = r.height();

        // If we are circle cropping, we want alpha channel, which is the third
        // param here.
        Bitmap croppedImage;
        try {
            croppedImage = Bitmap.createBitmap(width, height, mCircleCrop ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
        } catch (Exception e) {
            throw e;
        }
        if (croppedImage == null) {
            return;
        }

        {
            Canvas canvas = new Canvas(croppedImage);
            Rect dstRect = new Rect(0, 0, width, height);
            canvas.drawBitmap(mBitmap, r, dstRect, null);
        }

        if (mCircleCrop) {
            // OK, so what's all this about?
            // Bitmaps are inherently rectangular but we want to return
            // something that's basically a circle. So we fill in the
            // area around the circle with alpha. Note the all important
            // PortDuff.Mode.CLEAR.
            Canvas c = new Canvas(croppedImage);
            Path p = new Path();
            p.addCircle(width / 2F, height / 2F, width / 2F, Path.Direction.CW);
            c.clipPath(p, Region.Op.DIFFERENCE);
            c.drawColor(0x00000000, PorterDuff.Mode.CLEAR);
        }

        /* If the output is required to a specific size then scale or fill */
        if (mOutputX != 0 && mOutputY != 0) {

            if (mScale) {

                /* Scale the image to the required dimensions */
                Bitmap old = croppedImage;
                croppedImage = transform(new Matrix(), croppedImage, mOutputX,
                        mOutputY, mScaleUp);
                if (old != croppedImage) {

                    old.recycle();
                }
            } else {

                /*
                 * Don't scale the image crop it to the size requested. Create
                 * an new image with the cropped image in the center and the
                 * extra space filled.
                 */

                // Don't scale the image but instead fill it so it's the
                // required dimension
                Bitmap b = Bitmap.createBitmap(mOutputX, mOutputY,
                        Bitmap.Config.RGB_565);
                Canvas canvas = new Canvas(b);

                Rect srcRect = mCrop.getCropRect();
                Rect dstRect = new Rect(0, 0, mOutputX, mOutputY);

                int dx = (srcRect.width() - dstRect.width()) / 2;
                int dy = (srcRect.height() - dstRect.height()) / 2;

                /* If the srcRect is too big, use the center part of it. */
                srcRect.inset(Math.max(0, dx), Math.max(0, dy));

                /* If the dstRect is too big, use the center part of it. */
                dstRect.inset(Math.max(0, -dx), Math.max(0, -dy));

                /* Draw the cropped bitmap in the center */
                canvas.drawBitmap(mBitmap, srcRect, dstRect, null);

                /* Set the cropped bitmap as the new bitmap */
                croppedImage.recycle();
                croppedImage = b;
            }
        }

        // Return the cropped image directly or save it to the specified URI.
        Bundle myExtras = getIntent().getExtras();
        if (myExtras != null && (myExtras.getParcelable("data") != null || myExtras.getBoolean(RETURN_DATA))) {
            Bundle extras = new Bundle();
            extras.putParcelable(RETURN_DATA_AS_BITMAP, croppedImage);
            setResult(RESULT_OK, (new Intent()).setAction(ACTION_INLINE_DATA).putExtras(extras));
            finish();
        } else {
            final Bitmap b = croppedImage;
            startBackgroundJob(R.string.rte_processing_image, new Runnable() {
                public void run() {
                    saveOutput(b);
                }
            });
        }
    }

    private void saveOutput(Bitmap croppedImage) {
        if (mSaveUri != null) {
            OutputStream out = null;
            try {
                out = getContentResolver().openOutputStream(mSaveUri);
                if (out != null) {
                    croppedImage.compress(mOutputFormat, 90, out);
                }
            } catch (IOException ex) {
                Log.e(getClass().getSimpleName(), "Cannot open file: " + mSaveUri, ex);
                setResult(RESULT_CANCELED);
                finish();
                return;
            } finally {
                Helper.closeQuietly(out);
            }

            Bundle extras = new Bundle();
            Intent intent = new Intent(mSaveUri.toString());
            intent.putExtras(extras);
            intent.putExtra(IMAGE_SOURCE_FILE, mImageSource);
            intent.putExtra(IMAGE_DESTINATION_FILE, mImageDest);
            intent.putExtra(ORIENTATION_IN_DEGREES,
                    getOrientationInDegree(this));
            setResult(RESULT_OK, intent);

        } else {
            Log.e(getClass().getSimpleName(), "not defined image url");
        }
        croppedImage.recycle();
        finish();
    }

    @Override
    protected void onPause() {
        super.onPause();
        BitmapManager.instance().cancelThreadDecoding(mDecodingThreads);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mBitmap != null) {
            mBitmap.recycle();
        }
    }

    Runnable mRunFaceDetection = new Runnable() {
        float mScale = 1F;
        Matrix mImageMatrix;
        FaceDetector.Face[] mFaces = new FaceDetector.Face[3];
        int mNumFaces;

        // For each face, we create a HightlightView for it.
        private void handleFace(FaceDetector.Face f) {

            PointF midPoint = new PointF();

            int r = ((int) (f.eyesDistance() * mScale)) * 2;
            f.getMidPoint(midPoint);
            midPoint.x *= mScale;
            midPoint.y *= mScale;

            int midX = (int) midPoint.x;
            int midY = (int) midPoint.y;

            HighlightView hv = new HighlightView(mImageView);

            int width = mBitmap.getWidth();
            int height = mBitmap.getHeight();

            Rect imageRect = new Rect(0, 0, width, height);

            RectF faceRect = new RectF(midX, midY, midX, midY);
            faceRect.inset(-r, -r);
            if (faceRect.left < 0) {
                faceRect.inset(-faceRect.left, -faceRect.left);
            }

            if (faceRect.top < 0) {
                faceRect.inset(-faceRect.top, -faceRect.top);
            }

            if (faceRect.right > imageRect.right) {
                faceRect.inset(faceRect.right - imageRect.right, faceRect.right
                        - imageRect.right);
            }

            if (faceRect.bottom > imageRect.bottom) {
                faceRect.inset(faceRect.bottom - imageRect.bottom,
                        faceRect.bottom - imageRect.bottom);
            }

            hv.setup(mImageMatrix, imageRect, faceRect, mCircleCrop,
                    mAspectX != 0 && mAspectY != 0);

            mImageView.add(hv);
        }

        // Create a default HightlightView if we found no face in the picture.
        private void makeDefault() {

            HighlightView hv = new HighlightView(mImageView);

            int width = mBitmap.getWidth();
            int height = mBitmap.getHeight();

            Rect imageRect = new Rect(0, 0, width, height);

            // make the default size about 4/5 of the width or height
            /*
             * int cropWidth = Math.min(width, height) * 4 / 5; int cropHeight =
             * cropWidth;
             */
            int cropWidth = width;
            int cropHeight = height;

            if (mAspectX != 0 && mAspectY != 0) {

                if (mAspectX > mAspectY) {

                    cropHeight = cropWidth * mAspectY / mAspectX;
                } else {

                    cropWidth = cropHeight * mAspectX / mAspectY;
                }
            }

            int x = (width - cropWidth) / 2;
            int y = (height - cropHeight) / 2;

            RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
            hv.setup(mImageMatrix, imageRect, cropRect, mCircleCrop,
                    mAspectX != 0 && mAspectY != 0);

            mImageView.mHighlightViews.clear(); // Thong added for rotate

            mImageView.add(hv);
        }

        // Scale the image down for faster face detection.
        private Bitmap prepareBitmap() {

            if (mBitmap == null) {

                return null;
            }

            // 256 pixels wide is enough.
            if (mBitmap.getWidth() > 256) {

                mScale = 256.0F / mBitmap.getWidth();
            }
            Matrix matrix = new Matrix();
            matrix.setScale(mScale, mScale);
            return Bitmap.createBitmap(mBitmap, 0, 0, mBitmap.getWidth(),
                    mBitmap.getHeight(), matrix, true);
        }

        public void run() {

            mImageMatrix = mImageView.getImageMatrix();
            Bitmap faceBitmap = prepareBitmap();

            mScale = 1.0F / mScale;
            if (faceBitmap != null && mDoFaceDetection) {
                FaceDetector detector = new FaceDetector(faceBitmap.getWidth(),
                        faceBitmap.getHeight(), mFaces.length);
                mNumFaces = detector.findFaces(faceBitmap, mFaces);
            }

            if (faceBitmap != null && faceBitmap != mBitmap) {
                faceBitmap.recycle();
            }

            mHandler.post(new Runnable() {
                public void run() {
                    mWaitingToPick = mNumFaces > 1;
                    if (mNumFaces > 0) {
                        for (int i = 0; i < mNumFaces; i++) {
                            handleFace(mFaces[i]);
                        }
                    } else {
                        makeDefault();
                    }
                    mImageView.invalidate();
                    if (mImageView.mHighlightViews.size() == 1) {
                        mCrop = mImageView.mHighlightViews.get(0);
                        mCrop.setFocus(true);
                    }
                }
            });
        }
    };

    /*
     * Compute the sample size as a function of minSideLength and
     * maxNumOfPixels. minSideLength is used to specify that minimal width or
     * height of a bitmap. maxNumOfPixels is used to specify the maximal size in
     * pixels that are tolerable in terms of memory usage.
     *
     * The function returns a sample size based on the constraints. Both size
     * and minSideLength can be passed in as IImage.UNCONSTRAINED, which
     * indicates no care of the corresponding constraint. The functions prefers
     * returning a sample size that generates a smaller bitmap, unless
     * minSideLength = IImage.UNCONSTRAINED.
     */
    private Bitmap transform(Matrix scaler, Bitmap source, int targetWidth,
                             int targetHeight, boolean scaleUp) {

        int deltaX = source.getWidth() - targetWidth;
        int deltaY = source.getHeight() - targetHeight;
        if (!scaleUp && (deltaX < 0 || deltaY < 0)) {
            /*
             * In this case the bitmap is smaller, at least in one dimension,
             * than the target. Transform it by placing as much of the image as
             * possible into the target and leaving the top/bottom or left/right
             * (or both) black.
             */
            Bitmap b2 = Bitmap.createBitmap(targetWidth, targetHeight,
                    Bitmap.Config.ARGB_8888);
            Canvas c = new Canvas(b2);

            int deltaXHalf = Math.max(0, deltaX / 2);
            int deltaYHalf = Math.max(0, deltaY / 2);
            Rect src = new Rect(deltaXHalf, deltaYHalf, deltaXHalf
                    + Math.min(targetWidth, source.getWidth()), deltaYHalf
                    + Math.min(targetHeight, source.getHeight()));
            int dstX = (targetWidth - src.width()) / 2;
            int dstY = (targetHeight - src.height()) / 2;
            Rect dst = new Rect(dstX, dstY, targetWidth - dstX, targetHeight
                    - dstY);
            c.drawBitmap(source, src, dst, null);
            return b2;
        }
        float bitmapWidthF = source.getWidth();
        float bitmapHeightF = source.getHeight();

        float bitmapAspect = bitmapWidthF / bitmapHeightF;
        float viewAspect = (float) targetWidth / targetHeight;

        if (bitmapAspect > viewAspect) {
            float scale = targetHeight / bitmapHeightF;
            if (scale < .9F || scale > 1F) {
                scaler.setScale(scale, scale);
            } else {
                scaler = null;
            }
        } else {
            float scale = targetWidth / bitmapWidthF;
            if (scale < .9F || scale > 1F) {
                scaler.setScale(scale, scale);
            } else {
                scaler = null;
            }
        }

        Bitmap b1;
        if (scaler != null) {
            // this is used for minithumb and crop, so we want to mFilter here.
            b1 = Bitmap.createBitmap(source, 0, 0, source.getWidth(),
                    source.getHeight(), scaler, true);
        } else {
            b1 = source;
        }

        int dx1 = Math.max(0, b1.getWidth() - targetWidth);
        int dy1 = Math.max(0, b1.getHeight() - targetHeight);

        Bitmap b2 = Bitmap.createBitmap(b1, dx1 / 2, dy1 / 2, targetWidth,
                targetHeight);

        if (b1 != source) {
            b1.recycle();
        }

        return b2;
    }

    // Thong added for rotate
    private Bitmap rotateImage(Bitmap src, float degree) {
        // create new matrix
        Matrix matrix = new Matrix();
        // setup rotation degree
        matrix.postRotate(degree);
        Bitmap bmp = Bitmap.createBitmap(src, 0, 0, src.getWidth(),
                src.getHeight(), matrix, true);
        return bmp;
    }

    private int getOrientationInDegree(Activity activity) {
        int rotation = activity.getWindowManager().getDefaultDisplay()
                .getRotation();
        int degrees = 0;

        switch (rotation) {
            case Surface.ROTATION_0:
                degrees = 0;
                break;
            case Surface.ROTATION_90:
                degrees = 90;
                break;
            case Surface.ROTATION_180:
                degrees = 180;
                break;
            case Surface.ROTATION_270:
                degrees = 270;
                break;
        }

        return degrees;
    }

}


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

package com.onegravity.rteditor.media.choose;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;

import com.onegravity.rteditor.api.RTMediaFactory;
import com.onegravity.rteditor.api.format.RTFormat;
import com.onegravity.rteditor.api.media.RTAudio;
import com.onegravity.rteditor.api.media.RTImage;
import com.onegravity.rteditor.api.media.RTMedia;
import com.onegravity.rteditor.api.media.RTVideo;
import com.onegravity.rteditor.media.MonitoredActivity;
import com.onegravity.rteditor.media.choose.AudioChooserManager.AudioChooserListener;
import com.onegravity.rteditor.media.choose.ImageChooserManager.ImageChooserListener;
import com.onegravity.rteditor.media.choose.VideoChooserManager.VideoChooserListener;
import com.onegravity.rteditor.media.crop.CropImageActivity;
import com.onegravity.rteditor.utils.Constants;
import com.onegravity.rteditor.utils.Constants.MediaAction;

import org.greenrobot.eventbus.EventBus;

public class MediaChooserActivity extends MonitoredActivity implements
        ImageChooserListener,
        AudioChooserListener,
        VideoChooserListener {

    private static final String PREFIX = MediaChooserActivity.class.getSimpleName();

    public static final String EXTRA_MEDIA_ACTION = PREFIX + "EXTRA_MEDIA_ACTION";
    public static final String EXTRA_MEDIA_FACTORY = PREFIX + "EXTRA_MEDIA_FACTORY";

    private RTMediaFactory<RTImage, RTAudio, RTVideo> mMediaFactory;
    private MediaAction mMediaAction;

    transient private MediaChooserManager mMediaChooserMgr;

    private RTMedia mSelectedMedia;

    private static boolean mWorkInProgress;

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Bundle extras = getIntent().getExtras();
        if (extras != null) {
            String mediaAction = extras.getString(EXTRA_MEDIA_ACTION);
            mMediaAction = mediaAction == null ? null : MediaAction.valueOf(mediaAction);
            mMediaFactory = (RTMediaFactory<RTImage, RTAudio, RTVideo>) extras.getSerializable(EXTRA_MEDIA_FACTORY);
        }

        if (mMediaAction != null) {
            // retrieve parameters
            if (savedInstanceState != null) {
                mSelectedMedia = (RTMedia) savedInstanceState.getSerializable("mSelectedMedia");
            }

            switch (mMediaAction) {

                case PICK_PICTURE:
                case CAPTURE_PICTURE:
                    mMediaChooserMgr = new ImageChooserManager(this, mMediaAction, mMediaFactory, this, savedInstanceState);
                    break;

                case PICK_VIDEO:
                case CAPTURE_VIDEO:
                    mMediaChooserMgr = new VideoChooserManager(this, mMediaAction, mMediaFactory, this, savedInstanceState);
                    break;

                case PICK_AUDIO:
                case CAPTURE_AUDIO:
                    mMediaChooserMgr = new AudioChooserManager(this, mMediaAction, mMediaFactory, this, savedInstanceState);
                    break;
            }

            if (mMediaChooserMgr == null) {
                finish();
            } else if (!isWorkInProgress()) {
                setWorkInProgress(true);
                if (!mMediaChooserMgr.chooseMedia()) {
                    finish();
                }
            }
        } else {
            finish();
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        if (mSelectedMedia != null) {
            outState.putSerializable("mSelectedMedia", mSelectedMedia);
        }
    }

    private void setWorkInProgress(boolean value) {
        mWorkInProgress = value;
    }

    private boolean isWorkInProgress() {
        return mWorkInProgress;
    }

    @Override
    public void finish() {
        super.finish();
        setWorkInProgress(false);
    }

    // ****************************************** Listener Methods *******************************************

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == Activity.RESULT_OK) {

            if (requestCode == MediaAction.PICK_PICTURE.requestCode() && data != null) {
                mMediaChooserMgr.processMedia(MediaAction.PICK_PICTURE, data);
            } else if (requestCode == MediaAction.CAPTURE_PICTURE.requestCode()) {
                mMediaChooserMgr.processMedia(MediaAction.CAPTURE_PICTURE, data);    // data may be null here
            } else if (requestCode == Constants.CROP_IMAGE) {
                String path = data.getStringExtra(CropImageActivity.IMAGE_DESTINATION_FILE);
                if (path != null && mSelectedMedia instanceof RTImage) {
                    EventBus.getDefault().postSticky( new MediaEvent(mSelectedMedia) );
                    finish();
                }
            }

        } else {
            setResult(RESULT_CANCELED);
            finish();
        }
    }

    @Override
    /* ImageChooserListener */
    public void onImageChosen(final RTImage image) {
        mSelectedMedia = image;

        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                if (mMediaAction == MediaAction.CAPTURE_PICTURE) {
                    String filePath = image.getFilePath(RTFormat.SPANNED);

                    Intent intent = new Intent(MediaChooserActivity.this, CropImageActivity.class)

                            // tell CropImage activity to look for image to crop
                            .putExtra(CropImageActivity.IMAGE_SOURCE_FILE, filePath)
                            .putExtra(CropImageActivity.IMAGE_DESTINATION_FILE, filePath)

                                    // allow CropImage activity to re-scale image
                            .putExtra(CropImageActivity.SCALE, true)
                            .putExtra(CropImageActivity.SCALE_UP_IF_NEEDED, false)

                                    // no fixed aspect ratio
                            .putExtra(CropImageActivity.ASPECT_X, 0)
                            .putExtra(CropImageActivity.ASPECT_Y, 0);

                    // start activity CropImageActivity
                    startActivityForResult(intent, Constants.CROP_IMAGE);
                } else {
                    EventBus.getDefault().postSticky( new MediaEvent(mSelectedMedia) );
                    finish();
                }
            }
        });
    }

    @Override
    /* AudioChooserListener */
    public void onAudioChosen(RTAudio audio) {
        mSelectedMedia = audio;
        setWorkInProgress(false);
    }

    @Override
    /* VideoChooserListener */
    public void onVideoChosen(RTVideo video) {
        mSelectedMedia = video;
        setWorkInProgress(false);
    }

    @Override
    /* MediaChooserListener */
    public void onError(String reason) {
        Toast.makeText(this, reason, Toast.LENGTH_SHORT).show();
        setWorkInProgress(false);
    }

}


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

package com.onegravity.rteditor.converter.tagsoup;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Stack;
import java.util.regex.Matcher;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.NamespaceSupport;
import org.xml.sax.helpers.XMLFilterImpl;

import android.text.util.Linkify.MatchFilter;

import com.onegravity.rteditor.converter.tagsoup.util.StringEscapeUtils;

/**
 * Filter to write an XML document from a SAX event stream.
 *

 *

 * This class can be used by itself or as part of a SAX event stream: it takes
 * as input a series of SAX2 ContentHandler events and uses the information in
 * those events to write an XML document. Since this class is a filter, it can
 * also pass the events on down a filter chain for further processing (you can
 * use the XMLWriter to take a snapshot of the current state at any point in a
 * filter chain), and it can be used directly as a ContentHandler for a SAX2
 * XMLReader.
 *

 *

 *

 * The client creates a document by invoking the methods for standard SAX2
 * events, always beginning with the {@link #startDocument startDocument} method
 * and ending with the {@link #endDocument endDocument} method. There are
 * convenience methods provided so that clients to not have to create empty
 * attribute lists or provide empty strings as parameters; for example, the
 * method invocation
 *

 *

 *

 * w.startElement("foo");
 *

 *

 *

 * is equivalent to the regular SAX2 ContentHandler method
 *

 *

 *

 * w.startElement("", "foo", "", new AttributesImpl());
 *

 *

 *

 * Except that it is more efficient because it does not allocate a new empty
 * attribute list each time. The following code will send a simple XML document
 * to standard output:
 *

 *

 *

 * XMLWriter w = new XMLWriter();
 *
 * w.startDocument();
 * w.startElement("greeting");
 * w.characters("Hello, world!");
 * w.endElement("greeting");
 * w.endDocument();
 *

 *

 *

 * The resulting document will look like this:
 *

 *

 *

 * <?xml version="1.0" standalone="yes"?>
 *
 * <greeting>Hello, world!</greeting>
 *

 *

 *

 * In fact, there is an even simpler convenience method, <var>dataElement</var>,
 * designed for writing elements that contain only character data, so the code
 * to generate the document could be shortened to
 *

 *

 *

 * XMLWriter w = new XMLWriter();
 *
 * w.startDocument();
 * w.dataElement("greeting", "Hello, world!");
 * w.endDocument();
 *

 *

 *
Whitespace

 *

 *

 * According to the XML Recommendation, all whitespace in an XML
 * document is potentially significant to an application, so this class never
 * adds newlines or indentation. If you insert three elements in a row, as in
 *

 *

 *

 * w.dataElement("item", "1");
 * w.dataElement("item", "2");
 * w.dataElement("item", "3");
 *

 *

 *

 * you will end up with
 *

 *

 *

 * <item>1</item><item>3</item><item>3</item>
 *

 *

 *

 * You need to invoke one of the <var>characters</var> methods explicitly to add
 * newlines or indentation. Alternatively, you can use
 * {@link com.megginson.sax.DataWriter DataWriter}, which is derived from this
 * class -- it is optimized for writing purely data-oriented (or field-oriented)
 * XML, and does automatic linebreaks and indentation (but does not support
 * mixed content properly).
 *

 *

 *

 *
Namespace Support

 *

 *

 * The writer contains extensive support for XML Namespaces, so that a client
 * application does not have to keep track of prefixes and supply
 * <var>xmlns</var> attributes. By default, the XML writer will generate
 * Namespace declarations in the form _NS1, _NS2, etc., wherever they are
 * needed, as in the following example:
 *

 *

 *

 * w.startDocument();
 * w.emptyElement("http://www.foo.com/ns/", "foo");
 * w.endDocument();
 *

 *

 *

 * The resulting document will look like this:
 *

 *

 *

 * <?xml version="1.0" standalone="yes"?>
 *
 * <_NS1:foo xmlns:_NS1="http://www.foo.com/ns/"/>
 *

 *

 *

 * In many cases, document authors will prefer to choose their own prefixes
 * rather than using the (ugly) default names. The XML writer allows two methods
 * for selecting prefixes:
 *

 *

 *

 *
the qualified name

 *
the {@link #setPrefix setPrefix} method.

 *

 *

 *

 * Whenever the XML writer finds a new Namespace URI, it checks to see if a
 * qualified (prefixed) name is also available; if so it attempts to use the
 * name's prefix (as long as the prefix is not already in use for another
 * Namespace URI).
 *

 *

 *

 * Before writing a document, the client can also pre-map a prefix to a
 * Namespace URI with the setPrefix method:
 *

 *

 *

 * w.setPrefix("http://www.foo.com/ns/", "foo");
 * w.startDocument();
 * w.emptyElement("http://www.foo.com/ns/", "foo");
 * w.endDocument();
 *

 *

 *

 * The resulting document will look like this:
 *

 *

 *

 * <?xml version="1.0" standalone="yes"?>
 *
 * <foo:foo xmlns:foo="http://www.foo.com/ns/"/>
 *

 *

 *

 * The default Namespace simply uses an empty string as the prefix:
 *

 *

 *

 * w.setPrefix("http://www.foo.com/ns/", "");
 * w.startDocument();
 * w.emptyElement("http://www.foo.com/ns/", "foo");
 * w.endDocument();
 *

 *

 *

 * The resulting document will look like this:
 *

 *

 *

 * <?xml version="1.0" standalone="yes"?>
 *
 * <foo xmlns="http://www.foo.com/ns/"/>
 *

 *

 *

 * By default, the XML writer will not declare a Namespace until it is actually
 * used. Sometimes, this approach will create a large number of Namespace
 * declarations, as in the following example:
 *

 *

 *

 * <xml version="1.0" standalone="yes"?>
 *
 * <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
 *  <rdf:Description about="http://www.foo.com/ids/books/12345">
 *   <dc:title xmlns:dc="http://www.purl.org/dc/">A Dark Night</dc:title>
 *   <dc:creator xmlns:dc="http://www.purl.org/dc/">Jane Smith</dc:title>
 *   <dc:date xmlns:dc="http://www.purl.org/dc/">2000-09-09</dc:title>
 *  </rdf:Description>
 * </rdf:RDF>
 *

 *

 *

 * The "rdf" prefix is declared only once, because the RDF Namespace is used by
 * the root element and can be inherited by all of its descendants; the "dc"
 * prefix, on the other hand, is declared three times, because no higher element
 * uses the Namespace. To solve this problem, you can instruct the XML writer to
 * predeclare Namespaces on the root element even if they are not used there:
 *

 *

 *

 * w.forceNSDecl("http://www.purl.org/dc/");
 *

 *

 *

 * Now, the "dc" prefix will be declared on the root element even though it's
 * not needed there, and can be inherited by its descendants:
 *

 *

 *

 * <xml version="1.0" standalone="yes"?>
 *
 * <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 *             xmlns:dc="http://www.purl.org/dc/">
 *  <rdf:Description about="http://www.foo.com/ids/books/12345">
 *   <dc:title>A Dark Night</dc:title>
 *   <dc:creator>Jane Smith</dc:title>
 *   <dc:date>2000-09-09</dc:title>
 *  </rdf:Description>
 * </rdf:RDF>
 *

 *

 *

 * This approach is also useful for declaring Namespace prefixes that be used by
 * qualified names appearing in attribute values or character data.
 *

 *
 * @author David Megginson, david@megginson.com
 * @version 0.2
 * @see org.xml.sax.XMLFilter
 * @see org.xml.sax.ContentHandler
 */
public class HTMLWriter extends XMLFilterImpl implements LexicalHandler {

    // //////////////////////////////////////////////////////////////////
    // Tags to ignore
    // //////////////////////////////////////////////////////////////////
    private static Map<String, Map<String, String>> mTags2Ignore = new HashMap<String, Map<String, String>>();

    static {
        // meta refresh tag + iframe meta refresh
        HashMap<String, String> attributes = new HashMap<String, String>();
        attributes.put("http-equiv", "Refresh");
        mTags2Ignore.put("meta", attributes);
        mTags2Ignore.put("iframe", attributes);

        // video, audio tags with autoplay
        attributes = new HashMap<String, String>();
        attributes.put("autoplay", "autoplay#true");
        mTags2Ignore.put("audio", attributes);
        mTags2Ignore.put("video", attributes);
    }

    // //////////////////////////////////////////////////////////////////
    // Constants.
    // //////////////////////////////////////////////////////////////////

    public static final String CDATA_SECTION_ELEMENTS = "cdata-section-elements";
    public static final String DOCTYPE_PUBLIC = "doctype-public";
    public static final String DOCTYPE_SYSTEM = "doctype-system";
    public static final String ENCODING = "encoding";
    public static final String INDENT = "indent"; // currently ignored
    public static final String MEDIA_TYPE = "media-type"; // currently ignored
    public static final String METHOD = "method"; // currently html or xml
    public static final String OMIT_XML_DECLARATION = "omit-xml-declaration";
    public static final String STANDALONE = "standalone"; // currently ignored
    public static final String VERSION = "version";

    // //////////////////////////////////////////////////////////////////
    // Internal state.
    // //////////////////////////////////////////////////////////////////

    private Hashtable<String, String> prefixTable;
    private Hashtable<String, Boolean> forcedDeclTable;
    private Hashtable<String, String> doneDeclTable;
    private int elementLevel = 0;
    private Writer output;
    private NamespaceSupport nsSupport;
    private int prefixCounter = 0;
    private Properties outputProperties;
    private String outputEncoding = "";
    private boolean htmlMode = false;
    private boolean forceDTD = false;
    private boolean hasOutputDTD = false;
    private String overridePublic = null;
    private String overrideSystem = null;
    private String version = null;
    private String standalone = null;
    private boolean cdataElement = false;
    private boolean mOmitXHTMLNamespace;
    private Stack<String> mIgnoredTags;

    // //////////////////////////////////////////////////////////////////
    // Constructors.
    // //////////////////////////////////////////////////////////////////

    /**
     * Create a new XML writer.
     *

     *

     * Write to standard output.
     *

     */
    public HTMLWriter(boolean omitXHTMLNamespace) {
        nsSupport = new NamespaceSupport();
        prefixTable = new Hashtable<String, String>();
        forcedDeclTable = new Hashtable<String, Boolean>();
        doneDeclTable = new Hashtable<String, String>();
        outputProperties = new Properties();

        // we always generate HTML code...
        setOutputProperty(METHOD, "html");
        setOutputProperty(OMIT_XML_DECLARATION, "yes");
        mOmitXHTMLNamespace = omitXHTMLNamespace;
        mIgnoredTags = new Stack<String>();
    }

    // //////////////////////////////////////////////////////////////////
    // Public methods.
    // //////////////////////////////////////////////////////////////////

    /**
     * Reset the writer.
     *

     *

     * This method is especially useful if the writer throws an exception before
     * it is finished, and you want to reuse the writer for a new document. It
     * is usually a good idea to invoke {@link #flush flush} before resetting
     * the writer, to make sure that no output is lost.
     *

     *

     *

     * This method is invoked automatically by the {@link #startDocument
     * startDocument} method before writing a new document.
     *

     *

     *

     * Note: this method will not clear the prefix or
     * URI information in the writer or the selected output writer.
     *

     *
     * @throws SAXException
     * @see #flush
     */
    public void reset() throws SAXException {
        writeText4Links();
        elementLevel = 0;
        prefixCounter = 0;
        nsSupport.reset();
    }

    /**
     * Flush the output.
     *

     *

     * This method flushes the output stream. It is especially useful when you
     * need to make certain that the entire document has been written to output
     * but do not want to close the output stream.
     *

     *

     *

     * This method is invoked automatically by the {@link #endDocument
     * endDocument} method after writing a document.
     *

     *
     * @see #reset
     */
    public void flush() throws IOException, SAXException {
        writeText4Links();
        output.flush();
    }

    /**
     * Set a new output destination for the document.
     *
     * @param writer The output destination, or null to use standard output.
     * @return The current output writer.
     * @see #flush
     */
    public void setOutput(Writer writer) {
        if (writer == null) {
            output = new OutputStreamWriter(System.out);
        } else {
            output = writer;
        }
    }

    /**
     * Specify a preferred prefix for a Namespace URI.
     *

     *

     * Note that this method does not actually force the Namespace to be
     * declared; to do that, use the {@link #forceNSDecl(java.lang.String)
     * forceNSDecl} method as well.
     *

     *
     * @param uri    The Namespace URI.
     * @param prefix The preferred prefix, or "" to select the default Namespace.
     * @see #getPrefix
     * @see #forceNSDecl(java.lang.String)
     * @see #forceNSDecl(java.lang.String, java.lang.String)
     */
    public void setPrefix(String uri, String prefix) {
        prefixTable.put(uri, prefix);
    }

    /**
     * Get the current or preferred prefix for a Namespace URI.
     *
     * @param uri The Namespace URI.
     * @return The preferred prefix, or "" for the default Namespace.
     * @see #setPrefix
     */
    public String getPrefix(String uri) {
        return (String) prefixTable.get(uri);
    }

    /**
     * Force a Namespace to be declared on the root element.
     *

     *

     * By default, the XMLWriter will declare only the Namespaces needed for an
     * element; as a result, a Namespace may be declared many places in a
     * document if it is not used on the root element.
     *

     *

     *

     * This method forces a Namespace to be declared on the root element even if
     * it is not used there, and reduces the number of xmlns attributes in the
     * document.
     *

     *
     * @param uri The Namespace URI to declare.
     * @see #forceNSDecl(java.lang.String, java.lang.String)
     * @see #setPrefix
     */
    public void forceNSDecl(String uri) {
        forcedDeclTable.put(uri, Boolean.TRUE);
    }

    /**
     * Force a Namespace declaration with a preferred prefix.
     *

     *

     * This is a convenience method that invokes {@link #setPrefix setPrefix}
     * then {@link #forceNSDecl(java.lang.String) forceNSDecl}.
     *

     *
     * @param uri    The Namespace URI to declare on the root element.
     * @param prefix The preferred prefix for the Namespace, or "" for the default
     *               Namespace.
     * @see #setPrefix
     * @see #forceNSDecl(java.lang.String)
     */
    public void forceNSDecl(String uri, String prefix) {
        setPrefix(uri, prefix);
        forceNSDecl(uri);
    }

    // //////////////////////////////////////////////////////////////////
    // Methods from org.xml.sax.ContentHandler.
    // //////////////////////////////////////////////////////////////////

    /**
     * Write the XML declaration at the beginning of the document.
     *

     * Pass the event on down the filter chain for further processing.
     *
     * @throws org.xml.sax.SAXException If there is an error writing the XML declaration, or if a
     *                                  handler further down the filter chain raises an exception.
     * @see org.xml.sax.ContentHandler#startDocument
     */
    public void startDocument() throws SAXException {
        writeText4Links();
        reset();
        if (!("yes".equals(outputProperties.getProperty(OMIT_XML_DECLARATION,
                "no")))) {
            write("<?xml");
            if (version == null) {
                write(" version=\"1.0\"");
            } else {
                write(" version=\"");
                write(version);
                write("\"");
            }
            if (outputEncoding != null && outputEncoding != "") {
                write(" encoding=\"");
                write(outputEncoding);
                write("\"");
            }
            if (standalone == null) {
                write(" standalone=\"yes\"?>\n");
            } else {
                write(" standalone=\"");
                write(standalone);
                write("\"");
            }
        }
        super.startDocument();
    }

    /**
     * Write a newline at the end of the document.
     *

     * Pass the event on down the filter chain for further processing.
     *
     * @throws org.xml.sax.SAXException If there is an error writing the newline, or if a handler
     *                                  further down the filter chain raises an exception.
     * @see org.xml.sax.ContentHandler#endDocument
     */
    public void endDocument() throws SAXException {
        writeText4Links();
        write('\n');
        super.endDocument();
        try {
            flush();
        } catch (IOException e) {
            throw new SAXException(e);
        }
    }

    /**
     * Write a start tag.
     *

     * Pass the event on down the filter chain for further processing.
     *
     * @param uri       The Namespace URI, or the empty string if none is available.
     * @param localName The element's local (unprefixed) name (required).
     * @param qName     The element's qualified (prefixed) name, or the empty string
     *                  is none is available. This method will use the qName as a
     *                  template for generating a prefix if necessary, but it is not
     *                  guaranteed to use the same qName.
     * @param atts      The element's attribute list (must not be null).
     * @throws org.xml.sax.SAXException If there is an error writing the start tag, or if a
     *                                  handler further down the filter chain raises an exception.
     * @see org.xml.sax.ContentHandler#startElement
     */
    @Override
    public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException {
        writeText4Links();

        if (!ignoreElement(uri, localName, qName, atts)) {
            elementLevel++;
            nsSupport.pushContext();
            if (forceDTD && !hasOutputDTD) {
                startDTD(localName == null ? qName : localName, "", "");
            }
            write('<');
            writeName(uri, localName, qName, true);
            writeAttributes(atts);
            if (elementLevel == 1) {
                forceNSDecls();
            }
            if (!mOmitXHTMLNamespace || !"html".equalsIgnoreCase(localName)) {
                writeNSDecls();
            }
            write('>');
            if (htmlMode && (qName.equals("script") || qName.equals("style"))) {
                cdataElement = true;
            }
            if (htmlMode && localName.equals("a")) {
                mIgnoreChars = true;
            }
            super.startElement(uri, localName, qName, atts);
        }
    }

    private boolean ignoreElement(String uri, String localName, String qName, Attributes atts) {
        Map<String, String> tagAttrs = mTags2Ignore.get(qName.toLowerCase(Locale.US));
        if (tagAttrs != null) {
            for (String attrKey : tagAttrs.keySet()) {
                for (String attrValue : tagAttrs.get(attrKey).split("#")) {
                    String value = atts.getValue(attrKey);
                    if (!isNullOrEmpty(value) && attrValue.equalsIgnoreCase(value)) {
                        mIgnoredTags.push(qName);
                        return true;
                    }
                }
            }
        }

        return false;
    }

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

    /**
     * Write an end tag.
     *

     * Pass the event on down the filter chain for further processing.
     *
     * @param uri       The Namespace URI, or the empty string if none is available.
     * @param localName The element's local (unprefixed) name (required).
     * @param qName     The element's qualified (prefixed) name, or the empty string
     *                  is none is available. This method will use the qName as a
     *                  template for generating a prefix if necessary, but it is not
     *                  guaranteed to use the same qName.
     * @throws org.xml.sax.SAXException If there is an error writing the end tag, or if a handler
     *                                  further down the filter chain raises an exception.
     * @see org.xml.sax.ContentHandler#endElement
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        writeText4Links();
        if (!mIgnoredTags.isEmpty() && mIgnoredTags.peek().equalsIgnoreCase(qName)) {
            mIgnoredTags.pop();
        } else {
            if (!(htmlMode
                    && (uri.equals("http://www.w3.org/1999/xhtml") || uri.equals("")) && (qName.equals("area")
                    || qName.equals("base") || qName.equals("basefont")
                    || qName.equals("br") || qName.equals("col")
                    || qName.equals("frame") || qName.equals("hr")
                    || qName.equals("img") || qName.equals("input")
                    || qName.equals("isindex") || qName.equals("link")
                    || qName.equals("meta") || qName.equals("param")))) {
                write("</");
                writeName(uri, localName, qName, true);
                write('>');
            }
            if (elementLevel == 1) {
                write('\n');
            }
            if (htmlMode && localName.equals("a")) {
                mIgnoreChars = false;
            }
            cdataElement = false;
            super.endElement(uri, localName, qName);
            nsSupport.popContext();
            elementLevel--;
        }
    }

    /**
     * Write character data.
     *

     * Pass the event on down the filter chain for further processing.
     *
     * @param ch     The array of characters to write.
     * @param start  The starting position in the array.
     * @param length The number of characters to write.
     * @throws org.xml.sax.SAXException If there is an error writing the characters, or if a
     *                                  handler further down the filter chain raises an exception.
     * @see org.xml.sax.ContentHandler#characters
     */
    @Override
    public void characters(char ch[], int start, int len) throws SAXException {
        if (!cdataElement) {
            if (mIgnoreChars) {
                writeText4Links();
                writeEscUTF16(new String(ch), start, len, false);
            } else {
                collectText4Links(ch, start, len);
            }
        } else {
            writeText4Links();
            for (int i = start; i < start + len; i++) {
                write(ch[i]);
            }
        }

        super.characters(ch, start, len);
    }

    /**
     * Write ignorable whitespace.
     *

     * Pass the event on down the filter chain for further processing.
     *
     * @param ch     The array of characters to write.
     * @param start  The starting position in the array.
     * @param length The number of characters to write.
     * @throws org.xml.sax.SAXException If there is an error writing the whitespace, or if a
     *                                  handler further down the filter chain raises an exception.
     * @see org.xml.sax.ContentHandler#ignorableWhitespace
     */
    @Override
    public void ignorableWhitespace(char ch[], int start, int length) throws SAXException {
        writeText4Links();
        writeEscUTF16(new String(ch), start, length, false);
        super.ignorableWhitespace(ch, start, length);
    }

    /**
     * Write a processing instruction.
     *

     * Pass the event on down the filter chain for further processing.
     *
     * @param target The PI target.
     * @param data   The PI data.
     * @throws org.xml.sax.SAXException If there is an error writing the PI, or if a handler
     *                                  further down the filter chain raises an exception.
     * @see org.xml.sax.ContentHandler#processingInstruction
     */
    @Override
    public void processingInstruction(String target, String data) throws SAXException {
        writeText4Links();
        write("<?");
        write(target);
        write(' ');
        write(data);
        write("?>");
        if (elementLevel < 1) {
            write('\n');
        }
        super.processingInstruction(target, data);
    }

    // //////////////////////////////////////////////////////////////////
    // Internal methods.
    // //////////////////////////////////////////////////////////////////

    /**
     * Force all Namespaces to be declared.
     *

     * This method is used on the root element to ensure that the predeclared
     * Namespaces all appear.
     */
    private void forceNSDecls() {
        Enumeration<String> prefixes = forcedDeclTable.keys();
        while (prefixes.hasMoreElements()) {
            String prefix = (String) prefixes.nextElement();
            doPrefix(prefix, null, true);
        }
    }

    /**
     * Determine the prefix for an element or attribute name.
     *

     *
     * @param uri       The Namespace URI.
     * @param qName     The qualified name (optional); this will be used to indicate
     *                  the preferred prefix if none is currently bound.
     * @param isElement true if this is an element name, false if it is an attribute
     *                  name (which cannot use the default Namespace).
     */
    private String doPrefix(String uri, String qName, boolean isElement) {
        String defaultNS = nsSupport.getURI("");
        if ("".equals(uri)) {
            if (isElement && defaultNS != null)
                nsSupport.declarePrefix("", "");
            return null;
        }
        String prefix;
        if (isElement && defaultNS != null && uri.equals(defaultNS)) {
            prefix = "";
        } else {
            prefix = nsSupport.getPrefix(uri);
        }
        if (prefix != null) {
            return prefix;
        }
        prefix = (String) doneDeclTable.get(uri);
        if (prefix != null
                && ((!isElement || defaultNS != null) && "".equals(prefix) || nsSupport
                .getURI(prefix) != null)) {
            prefix = null;
        }
        if (prefix == null) {
            prefix = (String) prefixTable.get(uri);
            if (prefix != null
                    && ((!isElement || defaultNS != null) && "".equals(prefix) || nsSupport
                    .getURI(prefix) != null)) {
                prefix = null;
            }
        }
        if (prefix == null && qName != null && !"".equals(qName)) {
            int i = qName.indexOf(':');
            if (i == -1) {
                if (isElement && defaultNS == null) {
                    prefix = "";
                }
            } else {
                prefix = qName.substring(0, i);
            }
        }
        for (; prefix == null || nsSupport.getURI(prefix) != null; prefix = "__NS"
                + ++prefixCounter)
            ;
        nsSupport.declarePrefix(prefix, uri);
        doneDeclTable.put(uri, prefix);
        return prefix;
    }

    /**
     * Write a raw character.
     *
     * @param c The character to write.
     * @throws org.xml.sax.SAXException If there is an error writing the character, this method
     *                                  will throw an IOException wrapped in a SAXException.
     */
    private void write(char c) throws SAXException {
        try {
            output.write(c);
        } catch (IOException e) {
            throw new SAXException(e);
        }
    }

    /**
     * Write a raw string.
     *
     * @param s
     * @throws org.xml.sax.SAXException If there is an error writing the string, this method will
     *                                  throw an IOException wrapped in a SAXException
     */
    private void write(String s) throws SAXException {
        try {
            output.write(s);
        } catch (IOException e) {
            throw new SAXException(e);
        }
    }

    /**
     * Write out an attribute list, escaping values.
     *

     * The names will have prefixes added to them.
     *
     * @param atts The attribute list to write.
     * @throws org.xml.SAXException If there is an error writing the attribute list, this
     *                              method will throw an IOException wrapped in a
     *                              SAXException.
     */
    private void writeAttributes(Attributes atts) throws SAXException {
        int len = atts.getLength();
        for (int i = 0; i < len; i++) {
            write(' ');
            writeName(atts.getURI(i), atts.getLocalName(i), atts.getQName(i),
                    false);
            if (htmlMode
                    && booleanAttribute(atts.getLocalName(i), atts.getQName(i),
                    atts.getValue(i)))
                break;
            write("=\"");
            String s = atts.getValue(i);
            writeEscUTF16(s, 0, s.length(), true);
            write('"');
        }
    }

    private String[] booleans = {"checked", "compact", "declare", "defer",
            "disabled", "ismap", "multiple", "nohref", "noresize", "noshade",
            "nowrap", "readonly", "selected"};

    // Return true if the attribute is an HTML boolean from the above list.
    private boolean booleanAttribute(String localName, String qName,
                                     String value) {
        String name = localName;
        if (name == null) {
            int i = qName.indexOf(':');
            if (i != -1)
                name = qName.substring(i + 1, qName.length());
        }
        if (!name.equals(value))
            return false;
        for (int j = 0; j < booleans.length; j++) {
            if (name.equals(booleans[j]))
                return true;
        }
        return false;
    }

    /**
     * Write an array of data characters with escaping.
     *
     * @param ch       The array of characters.
     * @param start    The starting position.
     * @param length   The number of characters to use.
     * @param isAttVal true if this is an attribute value literal.
     * @throws org.xml.SAXException If there is an error writing the characters, this method
     *                              will throw an IOException wrapped in a SAXException.
     */
    private void writeEscUTF16(String s, int start, int length, boolean isAttVal) throws SAXException {
        String subString = s.substring(start, start + length);
        write(StringEscapeUtils.escapeHtml4(subString));
    }

    /**
     * Write out the list of Namespace declarations.
     *
     * @throws org.xml.sax.SAXException This method will throw an IOException wrapped in a
     *                                  SAXException if there is an error writing the Namespace
     *                                  declarations.
     */
    @SuppressWarnings("unchecked")
    private void writeNSDecls() throws SAXException {
        Enumeration<String> prefixes = (Enumeration<String>) nsSupport.getDeclaredPrefixes();
        while (prefixes.hasMoreElements()) {
            String prefix = (String) prefixes.nextElement();
            String uri = nsSupport.getURI(prefix);
            if (uri == null) {
                uri = "";
            }
            write(' ');
            if ("".equals(prefix)) {
                write("xmlns=\"");
            } else {
                write("xmlns:");
                write(prefix);
                write("=\"");
            }
            writeEscUTF16(uri, 0, uri.length(), true);
            write('\"');
        }
    }

    /**
     * Write an element or attribute name.
     *
     * @param uri       The Namespace URI.
     * @param localName The local name.
     * @param qName     The prefixed name, if available, or the empty string.
     * @param isElement true if this is an element name, false if it is an attribute
     *                  name.
     * @throws org.xml.sax.SAXException This method will throw an IOException wrapped in a
     *                                  SAXException if there is an error writing the name.
     */
    private void writeName(String uri, String localName, String qName,
                           boolean isElement) throws SAXException {
        String prefix = doPrefix(uri, qName, isElement);
        if (prefix != null && !"".equals(prefix)) {
            write(prefix);
            write(':');
        }
        if (localName != null && !"".equals(localName)) {
            write(localName);
        } else {
            int i = qName.indexOf(':');
            write(qName.substring(i + 1, qName.length()));
        }
    }

    // //////////////////////////////////////////////////////////////////
    // Default LexicalHandler implementation
    // //////////////////////////////////////////////////////////////////

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

    @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 {
        if (name == null)
            return; // can't cope
        if (hasOutputDTD)
            return; // only one DTD
        hasOutputDTD = true;
        write("<!DOCTYPE ");
        write(name);
        if (systemid == null)
            systemid = "";
        if (overrideSystem != null)
            systemid = overrideSystem;
        char sysquote = (systemid.indexOf('"') != -1) ? '\'' : '"';
        if (overridePublic != null)
            publicid = overridePublic;
        if (!(publicid == null || "".equals(publicid))) {
            char pubquote = (publicid.indexOf('"') != -1) ? '\'' : '"';
            write(" PUBLIC ");
            write(pubquote);
            write(publicid);
            write(pubquote);
            write(' ');
        } else {
            write(" SYSTEM ");
        }
        write(sysquote);
        write(systemid);
        write(sysquote);
        write(">\n");
    }

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

    // //////////////////////////////////////////////////////////////////
    // Output properties
    // //////////////////////////////////////////////////////////////////

    public String getOutputProperty(String key) {
        return outputProperties.getProperty(key);
    }

    public void setOutputProperty(String key, String value) {
        outputProperties.setProperty(key, value);
        if (key.equals(ENCODING)) {
            outputEncoding = value;
        } else if (key.equals(METHOD)) {
            htmlMode = value.equals("html");
        } else if (key.equals(DOCTYPE_PUBLIC)) {
            overridePublic = value;
            forceDTD = true;
        } else if (key.equals(DOCTYPE_SYSTEM)) {
            overrideSystem = value;
            forceDTD = true;
        } else if (key.equals(VERSION)) {
            version = value;
        } else if (key.equals(STANDALONE)) {
            standalone = value;
        }
    }

    // //////////////////////////////////////////////////////////////////
    // Linkifier code.
    // //////////////////////////////////////////////////////////////////

    private static final String[] LINK_SCHEMAS = new String[]{"http://", "https://", "rtsp://"};

    private static final MatchFilter URL_MATCH_FILTER = new MatchFilter() {
        public final boolean acceptMatch(CharSequence s, int start, int end) {
            return start == 0 || s.charAt(start - 1) != '@';
        }
    };

    private boolean mIgnoreChars;

    private StringBuffer mLastText4Links = new StringBuffer();

    private void collectText4Links(char ch[], int start, int len) throws SAXException {
        mLastText4Links.append(String.valueOf(ch, start, len));
    }

    private void writeText4Links() throws SAXException {
        if (mLastText4Links.length() > 0) {
            String text2Write = mLastText4Links.toString();

            Writer tmp = output;
            output = new StringWriter();

            Matcher m = Patterns.WEB_URL.matcher(mLastText4Links);
            int lastLinkEnd = 0;
            while (m.find()) {
                if (URL_MATCH_FILTER == null || URL_MATCH_FILTER.acceptMatch(mLastText4Links, m.start(), m.start())) {
                    // write leading characters
                    writeEscUTF16(text2Write, lastLinkEnd, m.start() - lastLinkEnd, false);

                    // write link
                    try {
                        String linkText = m.group(0);
                        String link = makeUrl(linkText, LINK_SCHEMAS, m);
                        output.append("");
                        writeEscUTF16(linkText, 0, linkText.length(), false);
                        output.append("");
                    } catch (IOException ignore) {
                    }

                    lastLinkEnd = m.end();
                }
            }

            // write tailing characters
            if (lastLinkEnd < text2Write.length()) {
                writeEscUTF16(text2Write, lastLinkEnd, text2Write.length() - lastLinkEnd, false);
            }

            String text2WriteString = output.toString();
            output = tmp;
            write(text2WriteString);
            mLastText4Links.setLength(0);
        }
    }

    private String makeUrl(String url, String[] prefixes, Matcher m) {
        boolean hasPrefix = false;

        for (int i = 0; i < prefixes.length; i++) {
            if (url.regionMatches(true, 0, prefixes[i], 0, prefixes[i].length())) {
                hasPrefix = true;

                // Fix capitalization if necessary
                if (!url.regionMatches(false, 0, prefixes[i], 0, prefixes[i].length())) {
                    url = prefixes[i] + url.substring(prefixes[i].length());
                }

                break;
            }
        }

        if (!hasPrefix) {
            url = prefixes[0] + url;
        }

        return url.replace("\u00a0", "");    // replace("\u00a0","") removes
    }

}


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

package com.onegravity.rteditor.converter.tagsoup;

/**
 * The internal representation of an actual element (not an element type). An
 * Element has an element type, attributes, and a successor Element for use in
 * constructing stacks and queues of Elements.
 *
 * @see ElementType
 * @see AttributesImpl
 */
public class Element {

    private ElementType theType; // type of element
    private AttributesImpl theAtts; // attributes of element
    private Element theNext; // successor of element
    private boolean preclosed; // this element has been preclosed

    /**
     * Return an Element from a specified ElementType.
     *
     * @param type              The element type of the newly constructed element
     * @param defaultAttributes True if default attributes are wanted
     */

    public Element(ElementType type, boolean defaultAttributes) {
        theType = type;
        if (defaultAttributes)
            theAtts = new AttributesImpl(type.atts());
        else
            theAtts = new AttributesImpl();
        theNext = null;
        preclosed = false;
    }

    /**
     * Return the element type.
     *
     * @return The element type.
     */

    public ElementType type() {
        return theType;
    }

    /**
     * Return the attributes as an AttributesImpl object. Returning an
     * AttributesImpl makes the attributes mutable.
     *
     * @return The attributes
     * @see AttributesImpl
     */
    public AttributesImpl atts() {
        return theAtts;
    }

    /**
     * Return the next element in an element stack or queue.
     *
     * @return The next element
     */

    public Element next() {
        return theNext;
    }

    /**
     * Change the next element in an element stack or queue.
     *
     * @param next The new next element
     */

    public void setNext(Element next) {
        theNext = next;
    }

    /**
     * Return the name of the element's type. Convenience method.
     *
     * @return The element type name
     */

    public String name() {
        return theType.name();
    }

    /**
     * Return the namespace name of the element's type. Convenience method.
     *
     * @return The element type namespace name
     */

    public String namespace() {
        return theType.namespace();
    }

    /**
     * Return the local name of the element's type. Convenience method.
     *
     * @return The element type local name
     */

    public String localName() {
        return theType.localName();
    }

    /**
     * Return the content model vector of the element's type. Convenience
     * method.
     *
     * @return The content model vector
     */

    public int model() {
        return theType.model();
    }

    /**
     * Return the member-of vector of the element's type. Convenience method.
     *
     * @return The member-of vector
     */

    public int memberOf() {
        return theType.memberOf();
    }

    /**
     * Return the flags vector of the element's type. Convenience method.
     *
     * @return The flags vector
     */

    public int flags() {
        return theType.flags();
    }

    /**
     * Return the parent element type of the element's type. Convenience method.
     *
     * @return The parent element type
     */

    public ElementType parent() {
        return theType.parent();
    }

    /**
     * Return true if the type of this element can contain the type of another
     * element. Convenience method.
     *
     * @param other The other element
     */

    public boolean canContain(Element other) {
        return theType.canContain(other.theType);
    }

    /**
     * Set an attribute and its value into this element.
     *
     * @param name  The attribute name (Qname)
     * @param type  The attribute type
     * @param value The attribute value
     */

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

    /**
     * Make this element anonymous. Remove any <tt>id</tt> or <tt>name</tt>
     * attribute present in the element's attributes.
     */

    public void anonymize() {
        for (int i = theAtts.getLength() - 1; i >= 0; i--) {
            if (theAtts.getType(i).equals("ID")
                    || theAtts.getQName(i).equals("name")) {
                theAtts.removeAttribute(i);
            }
        }
    }

    /**
     * Clean the attributes of this element. Attributes with null name (the name
     * was ill-formed) or null value (the attribute was present in the element
     * type but not in this actual element) are removed.
     */

    public void clean() {
        for (int i = theAtts.getLength() - 1; i >= 0; i--) {
            String name = theAtts.getLocalName(i);
            if (theAtts.getValue(i) == null || name == null
                    || name.length() == 0) {
                theAtts.removeAttribute(i);
                continue;
            }
        }
    }

    /**
     * Force this element to preclosed status, meaning that an end-tag has been
     * seen but the element cannot yet be closed for structural reasons.
     */

    public void preclose() {
        preclosed = true;
    }

    /**
     * Return true if this element has been preclosed.
     */

    public boolean isPreclosed() {
        return preclosed;
    }

}


package io.axway.iron.spi.aws.kinesis;

import javax.annotation.*;
import com.amazonaws.services.kinesis.AmazonKinesis;
import com.amazonaws.services.kinesis.AmazonKinesisClientBuilder;
import com.amazonaws.services.kinesis.model.DescribeStreamRequest;
import com.amazonaws.services.kinesis.model.DescribeStreamResult;
import com.amazonaws.services.kinesis.model.LimitExceededException;
import com.amazonaws.services.kinesis.model.ResourceNotFoundException;

import static io.axway.iron.spi.aws.AwsUtils.*;

/**
 * Some AWS Kinesis utils.
 */
public final class AwsKinesisUtils {
    /**
     * Stream Status when the steam is active.
     */
    static final String ACTIVE_STREAM_STATUS = "ACTIVE";

    public static final int DEFAULT_RETRY_DURATION_IN_MILLIS = 5000;
    public static final int DEFAULT_RETRY_COUNT = 5;

    private static final int MIN_3 = 1000 * 60 * 3;

    /**
     * Create a AmazonKinesis client configured with some optional properties (can also be configured using environment variables):
     *
     * @param accessKey - aws access key (optional)
     * @param secretKey - aws secret key (optional)
     * @param endpoint - kinesis endpoint (optional*)
     * @param port - kinesis port (optional*)
     * @param region - aws region (optional*)
     * (*) to configure the endpoint, endpoint, port and region must be provided.
     * @return a configured AmazonKinesis client
     */
    public static AmazonKinesis buildKinesisClient(@Nullable String accessKey, @Nullable String secretKey, //
                                                   @Nullable String endpoint, @Nullable Integer port, @Nullable String region) {
        AmazonKinesisClientBuilder builder = AmazonKinesisClientBuilder.standard();
        setAws(builder, accessKey, secretKey, endpoint, port, region);
        return builder.build();
    }

    /**
     * Ensure that the Kinesis Stream exists (creates it if does not exist).
     *
     * @param kinesisClient Kinesis client
     * @param streamName Stream name
     */
    public static void ensureStreamExists(AmazonKinesis kinesisClient, String streamName) {
        createStreamIfNotExists(kinesisClient, streamName, 1);
        waitStreamActivation(kinesisClient, streamName, MIN_3);
    }

    /**
     * Create a stream if it does not already exist.
     *
     * @param kinesis AmazonKinesis client
     * @param streamName the name of the stream
     */
    private static void createStreamIfNotExists(AmazonKinesis kinesis, String streamName, int shardCount) {
        performAmazonActionWithRetry("createStream", () -> {
            DescribeStreamRequest describeStreamRequest = new DescribeStreamRequest().withStreamName(streamName).withLimit(1);
            try {
                kinesis.describeStream(describeStreamRequest);
            } catch (ResourceNotFoundException e) {
                kinesis.createStream(streamName, shardCount);
            }
            return null;
        }, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_DURATION_IN_MILLIS);
    }

    /**
     * Waits that the stream has been created.
     */
    private static void waitStreamActivation(AmazonKinesis consumer, String streamName, long streamCreationTimeoutMillis) {
        DescribeStreamRequest describeStreamRequest = new DescribeStreamRequest().withStreamName(streamName).withLimit(1);
        DescribeStreamResult describeStreamResult = null;
        String streamStatus = null;
        long endTime = System.currentTimeMillis() + streamCreationTimeoutMillis;
        do {
            try {
                describeStreamResult = consumer.describeStream(describeStreamRequest);
                streamStatus = describeStreamResult.getStreamDescription().getStreamStatus();
                if (ACTIVE_STREAM_STATUS.equals(streamStatus)) {
                    break;
                }
                Thread.sleep(100);
            } catch (ResourceNotFoundException | LimitExceededException ignored) {
                // ignored
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new AwsKinesisException("Thread interrupted while waiting for stream activation", args -> args.add("streamName", streamName), e);
            }
        } while (System.currentTimeMillis() < endTime);
        if (describeStreamResult == null || streamStatus == null || !streamStatus.equals(ACTIVE_STREAM_STATUS)) {
            throw new AwsKinesisException("Stream never went active",
                                          args -> args.add("streamName", streamName).add("streamCreationTimeoutMillis", streamCreationTimeoutMillis));
        }
    }

    private AwsKinesisUtils() {
        // utility class
    }
}