text:
package me.scai.plato.android.views;

import java.io.BufferedInputStream;
import java.io.BufferedReader;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.Executors;

import java.net.URL;
import java.util.concurrent.atomic.AtomicBoolean;

import me.scai.handwriting.CWrittenTokenSet;
import me.scai.handwriting.CWrittenTokenSetNoStroke;
import me.scai.parsetree.HandwritingEngineException;
import me.scai.parsetree.Node;
import me.scai.parsetree.ParseTreeMathTexifier;
import me.scai.parsetree.TokenSetParserException;
import me.scai.parsetree.TokenSetParserOutput;
import me.scai.parsetree.evaluation.PlatoVarMap;
import me.scai.plato.AndroidWebCient.AndroidPlatoWebClient;
import me.scai.plato.AndroidWebCient.PlatoNotesAction;
import me.scai.plato.android.tasks.AndroidWebClientAsyncTask;
import me.scai.plato.android.utils.EventBus;
import me.scai.plato.android.responses.PlatoClientTaskResponse;
import me.scai.plato.android.requests.PlatoWebClientForceSetTokenNameRequest;
import me.scai.plato.android.requests.PlatoWebClientInjectStateRequest;
import me.scai.plato.android.requests.PlatoWebClientMergeStrokesRequest;
import me.scai.plato.android.requests.PlatoWebClientMoveTokenRequest;
import me.scai.plato.android.requests.PlatoWebClientTaskRequest;
import me.scai.plato.android.R;
import me.scai.plato.android.events.WrittenTokenSetChangedEvent;
import me.scai.plato.android.helpers.ClientInfoHelper;
import me.scai.plato.android.helpers.FileSystemHelper;
import me.scai.plato.android.helpers.NetworkStatusHelper;

import android.app.Activity;
import android.content.Context;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.graphics.Point;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.EditText;
import android.widget.Toast;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.squareup.otto.Bus;

import org.apache.commons.lang3.exception.ExceptionUtils;

import me.scai.handwriting.StrokeCurator;
import me.scai.handwriting.StrokeCuratorConfigurable;
import me.scai.handwriting.TokenRecogEngine;
import me.scai.handwriting.TokenRecogEngineIMG;
import me.scai.handwriting.TokenRecogEngineSDV;
import me.scai.handwriting.CWrittenToken;
import me.scai.parsetree.GraphicalProductionSet;
import me.scai.parsetree.TerminalSet;
import me.scai.parsetree.TokenSetParser;
import me.scai.parsetree.ParseTreeStringizer;
import me.scai.parsetree.evaluation.ParseTreeEvaluator;

import me.scai.plato.android.events.TokenRecogCandidatesEvent;

public class PlatoBoard extends PlatoMultiTouchEventView {
    /* Constants */
	private static final boolean bDebug = true;
    private static final Gson gson = new Gson();

    private static final String SERIALIZATION_STROKE_CURATOR_STATE_KEY = "strokeCuratorState";
    private static final String SERIALIZATION_PATHS_KEY                = "paths";
    private static final String SERIALIZATION_NPOINTS_KEY              = "nPoints";
    private static final String SERIALIZATION_FIRST_POINTS_KEY         = "firstPoints";

    private static final String WT_FILE_EXT = ".wt";
    private static final String IM_FILE_EXT = ".im";

	/* For SDV-type token engine */
	private int npPerStroke = 16;
	private int maxNumStrokes = 4;

	/* For IMG-type token engine */
	private static final int WT_W = 16;
	private static final int WT_H = 16;

	// private static int DOWNSAMPLE_HEIGHT = 10;
	//  private static int DOWNSAMPLE_WIDTH = 10;
	private static final String LETTER_START = "L_";
	private static final String TOKENSET_START = "TS_";
	private static final String TAG = "OCR";

	/* Sub directory names */
	private static final String LETTERDIR = "letters";
	private static final String ENGINEDIR = "engines";
	private static final String TOKENSETDIR = "tokensets";

	private String externalDir;
	private String letterDir;
	private String engineDir;
	private String tokenSetDir;
	// private Engine engine;

	/* Token engine settings: Default */
	private String tokenEngineType = "SDV"; /* SDV or IMG */

	private int hiddenLayerSize1 = 100;
	private int hiddenLayerSize2 = 0;
	private int trainMaxIter = 100;
	private double trainThreshErr = 0.01;

	/* Feature settings */
	private boolean bIncludeTokenSize = false;
	private boolean bIncludeTokenWHRatio = false; /*
												 * TODO: For SDV, the WHRatio
												 * doesn't quite work yet.
												 */
	private boolean bIncludeTokenNumStrokes = true;

	private static final ScheduledExecutorService worker = Executors
			.newSingleThreadScheduledExecutor();

	public TokenRecogEngine tokEngine = new TokenRecogEngineSDV(
			hiddenLayerSize1, hiddenLayerSize2, trainMaxIter, trainThreshErr);
	private String[] letters = getResources().getStringArray(R.array.letters);

	/* Parameters and variables for timing */
	protected double recogIdleTime_ms = 500.0; /* Unit: ms */

	protected boolean flagWaitTillRecog = false;
	protected boolean flagRecog = false;

	private static final ScheduledExecutorService flagSettingWorker = Executors
			.newSingleThreadScheduledExecutor();
	private ScheduledFuture<?> strokeRecogFuture;

	/* Variables and parameters for graphical token-set parser */
	private TokenSetParser tokenSetParser;
	private ParseTreeStringizer stringizer;
	private ParseTreeEvaluator evaluator;
    private ParseTreeMathTexifier mathTexifier;

	private StrokeCurator strokeCurator; // = new StrokeCurator();

	private CWrittenToken lastSavedWrittenToken; /* For storing the last saved written token, to allow correction */
	private String lastSavedImageFileBase;

	/* Plato web client */
	private AndroidPlatoWebClient webClient;
	private String platoHandwritingEndpoint;
	private String platoTokenRecogEndpoint;

//    private TextView webClientStatusDisplay;
    private Context baseContext;

    private static final Map<String, Long> parserTimeoutMillis = new HashMap<String, Long>();

    private AsyncTask asyncTask;
    private boolean parsingCancelledDueToTimeOut = false;

    private Thread parserKiller;

	private PlatoNotesAction cachedAction;
	private String[] cachedActionParams;

    private AtomicBoolean requiresWebClientStateInjection = new AtomicBoolean(false);

	/* ************************ Inner classes ************************ */
	protected class SetFlagRecogTask implements Runnable {
		public void run() {
			flagRecog = true;
			flagWaitTillRecog = false;

			// System.out.println("Set flagRecog to " + flagRecog); // DEBUG

			/* Recognize the token formed by the unincorporated strokes */
			// recognizeNewStrokes(); // WORK
		}
	}

	/* **************************** Methods **************************** */
    public synchronized JsonElement getStateSerialization() {
        JsonObject state = new JsonObject();

        state.add(SERIALIZATION_PATHS_KEY,  gson.toJsonTree(paths));
        state.add(SERIALIZATION_NPOINTS_KEY, gson.toJsonTree(nPoints));
        state.add(SERIALIZATION_FIRST_POINTS_KEY, gson.toJsonTree(firstPoints));

        state.add(SERIALIZATION_STROKE_CURATOR_STATE_KEY, strokeCurator.getStateSerialization());

        return state;
    }

    public synchronized void injectSerializedState(JsonObject jsonObj) {
        /* paths */
        JsonArray jsonPaths = jsonObj.get(SERIALIZATION_PATHS_KEY).getAsJsonArray();
        paths = new LinkedList<ExtendedPath>();
        for (int i = 0; i < jsonPaths.size(); ++i) {
            paths.add(gson.fromJson(jsonPaths.get(i), ExtendedPath.class));
        }

        /* nPoints */
        JsonArray jsonNPoints = jsonObj.get(SERIALIZATION_NPOINTS_KEY).getAsJsonArray();
        nPoints = new LinkedList<Integer>();
        for (int i = 0; i < jsonNPoints.size(); ++i) {
            nPoints.add(jsonNPoints.get(i).getAsInt());
        }

        /* firstPoints */
        JsonArray jsonFirstPoints = jsonObj.get(SERIALIZATION_FIRST_POINTS_KEY).getAsJsonArray();
        firstPoints = new LinkedList<Point>();
        for (int i = 0; i < jsonFirstPoints.size(); ++i) {
            firstPoints.add(gson.fromJson(jsonFirstPoints.get(i), Point.class));
        }

        strokeCurator.injectSerializedState(jsonObj.get(SERIALIZATION_STROKE_CURATOR_STATE_KEY).getAsJsonObject());

        updateWrittenTokenSet();

        orderWebClientStateInjection();

        if (isWebClientReady()) {
            if (requiresWebClientStateInjection.getAndSet(false)) {
                final JsonObject prevState = strokeCurator.getStateSerialization().getAsJsonObject();
                PlatoWebClientTaskRequest stateInjectionReq = new PlatoWebClientInjectStateRequest(webClient, prevState);
                new AndroidWebClientAsyncTask().execute(stateInjectionReq);

            }
        }
    }

    public synchronized void orderWebClientStateInjection() {
        requiresWebClientStateInjection.set(true);
    }

	public synchronized void createPlatoWebClient() throws HandwritingEngineException {
        JsonObject prevState = null;
        if (requiresWebClientStateInjection.getAndSet(false)) {
            prevState = strokeCurator.getStateSerialization().getAsJsonObject();
        }

		try {
            Activity activity = (Activity) this.getContext();

			webClient = new AndroidPlatoWebClient(platoHandwritingEndpoint,
			                           		      platoTokenRecogEndpoint,
                                                  ClientInfoHelper.getCustomClientData(activity),
												  activity.getString(R.string.platoServerBypassSecurityToken));

			if (prevState != null) {
				webClient.injectState(prevState);
			}

		} catch (HandwritingEngineException exc) {
			throw exc;
		}

	}

    public synchronized boolean isWebClientReady() {
		if (webClient != null) {
			boolean isNetwork = NetworkStatusHelper.isNetworkAvailable(baseContext);

			if (!isNetwork) {
				webClient = null;
			}

			return isNetwork;
		} else {
			return false;
		}
    }

	public AndroidPlatoWebClient getWebClient() {
		return webClient;
	}

	/* Engine training parameter getters */
	public double getTrainThreshErr() {
		return trainThreshErr;
	}

	/* Engine training maximum number of iterations */
	public int getTrainMaxIter() {
		return trainMaxIter;
	}

	public boolean getIncludeTokenSize() {
		return bIncludeTokenSize;
	}

	public boolean getIncludeTokenWHRatio() {
		return bIncludeTokenWHRatio;
	}

	public boolean getIncludeTokenNumStrokes() {
		return bIncludeTokenNumStrokes;
	}

	/* Engine training parameter setters */
	public void setTrainThreshErr(double t_trainThreshErr) {
		trainThreshErr = t_trainThreshErr;

		/* Refresh engine */
		if (tokenEngineType.equals("SDV")) {
			tokEngine = new TokenRecogEngineSDV(hiddenLayerSize1,
					hiddenLayerSize2, trainMaxIter, trainThreshErr);
		} else if (tokenEngineType.equals("IMG")) {
			tokEngine = new TokenRecogEngineIMG(hiddenLayerSize1,
					hiddenLayerSize2, trainMaxIter, trainThreshErr);
		} else {
			throw new RuntimeException("Unrecognized token engine type: "
					+ tokenEngineType);
		}

		setTokenEngineFeatures();
	}

	public void setTrainMaxIter(int t_trainMaxIter) {
		if (t_trainMaxIter > 0) {
			trainMaxIter = t_trainMaxIter;
		} else {
			System.err.println("ERROR: input trainMaxIter has an non-positive value.");
		}

		/* Refresh engine */
		if (tokenEngineType.equals("SDV")) {
			tokEngine = new TokenRecogEngineSDV(hiddenLayerSize1,
					hiddenLayerSize2, trainMaxIter, trainThreshErr);
		} else if (tokenEngineType.equals("IMG")) {
			tokEngine = new TokenRecogEngineIMG(hiddenLayerSize1,
					hiddenLayerSize2, trainMaxIter, trainThreshErr);
		} else {
			throw new RuntimeException("Unrecognized token engine type: "
					+ tokenEngineType);
		}

		setTokenEngineFeatures();
	}

	public void setIncludeTokenSize(boolean b) {
		bIncludeTokenSize = b;

		setTokenEngineFeatures();
	}

	public void setIncludeTokenWHRatio(boolean b) {
		bIncludeTokenWHRatio = b;

		setTokenEngineFeatures();
	}

	public void setIncludeTokenNumStrokes(boolean b) {
		bIncludeTokenNumStrokes = b;

		setTokenEngineFeatures();
	}

	private void setTokenEngineFeatures() {
		int[] ivs = null;
		boolean[] bvs = null;
		if (tokenEngineType.equals("SDV")) {
			ivs = new int[2];
			ivs[0] = npPerStroke;
			ivs[1] = maxNumStrokes;

			bvs = new boolean[3];
			bvs[0] = bIncludeTokenSize;
			bvs[1] = bIncludeTokenWHRatio;
			bvs[2] = bIncludeTokenNumStrokes;
		} else if (tokenEngineType.equals("IMG")) {
			ivs = new int[2];
			ivs[0] = WT_W;
			ivs[1] = WT_H;

			bvs = new boolean[3];
			bvs[0] = bIncludeTokenSize;
			bvs[1] = bIncludeTokenWHRatio;
			bvs[2] = bIncludeTokenNumStrokes;
		} else {
			throw new RuntimeException("Unrecognized token engine type: "
					+ tokenEngineType);
		}

		tokEngine.setFeatures(ivs, bvs);
	}

	/* Constructor */
	public PlatoBoard(Context context, AttributeSet attrs) {
		super(context, attrs);

        platoHandwritingEndpoint = context.getString(R.string.plato_handwriting_endpoint);
        platoTokenRecogEndpoint  = context.getString(R.string.plato_token_recog_endpoint);

        letterDir = FileSystemHelper.ensureExternalDirectory(getContext(), LETTERDIR);
        engineDir = FileSystemHelper.ensureExternalDirectory(getContext(), ENGINEDIR);

//		File filesDir = getContext().getExternalFilesDir(null);
//
//		if (filesDir == null) {
//			AlertDialog errDialog = new AlertDialog.Builder(context).setMessage(R.string.externalFilesDirMissingErrorMsg)
//				.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
//					@Override
//					public void onClick(DialogInterface dialog, int whichButton) {
//						dialog.dismiss();
//						System.exit(1); /* TODO: Make it work */
//					}
//				}).create();
//
//			errDialog.show();
//		}
//
//		String filesPath = filesDir.getPath();
//
////		externalDir = getContext().getExternalFilesDir(null).getPath();
//		externalDir = filesPath;
//
//		// externalDir = (externalDir.endsWith("/")? externalDir :
//		// externalDir+"/") + APPDIR;
//		letterDir = externalDir.endsWith("/") ? externalDir : externalDir + "/";
//		letterDir = letterDir + LETTERDIR;
//		engineDir = externalDir.endsWith("/") ? externalDir : externalDir + "/";
//		engineDir = engineDir + ENGINEDIR;
//		File lDir = new File(letterDir);
//		if (!lDir.isDirectory()) {
//			lDir.mkdir();
//		}
//		File eDir = new File(engineDir);
//		if (!eDir.isDirectory()) {
//			eDir.mkdir();
//		}
//		if (!lDir.isDirectory()) {
//			Log.e(TAG, "Directory doesn't exist : " + letterDir);
//		}
//		if (!eDir.isDirectory()) {
//			Log.e(TAG, "Directory doesn't exist : " + engineDir);
//		}

		/* Set engine feature set */
		setTokenEngineFeatures();

        parserTimeoutMillis.put("local", 30L * 1000L); // TODO: Move to config file
        parserTimeoutMillis.put("web", 30L * 1000L);   // TODO: Move to config file
	}

	/* Create graphical language parser */
	public void createParser(URL terminalSetUrl,
                             URL graphicalProductionSetUrl) {
		/* Create terminal set */
		TerminalSet termSet = null;
		try {
			termSet = TerminalSet.createFromJsonAtUrl(terminalSetUrl);
		} catch (Exception e) {
			Toast.makeText(
					getContext(),
					"ERROR: Failed to create TerminalSet from URL: \""
							+ terminalSetUrl + "\"", Toast.LENGTH_LONG).show();
		}

		/* Create graphical production set */
		GraphicalProductionSet gpSet = null;
		try {
//			gpSet = GraphicalProductionSet.createFromFile(graphicalProductionSetFN, termSet);
			gpSet = GraphicalProductionSet.createFromUrl(graphicalProductionSetUrl, termSet);
		} catch (FileNotFoundException fnfe) {
			Toast.makeText(
//					getContext(),
//					"ERROR: Failed to create GraphicalProductionSet from file: "
//							+ graphicalProductionSetFN, Toast.LENGTH_LONG)
//					.show();
					getContext(),
					"ERROR: Failed to create GraphicalProductionSet from URL: \""
							+ graphicalProductionSetUrl + "\"", Toast.LENGTH_LONG)
					.show();
		} catch (IOException e) {
			Toast.makeText(
//					getContext(),
//					"ERROR: Failed to create GraphicalProductionSet from file: "
//							+ graphicalProductionSetFN, Toast.LENGTH_LONG)
//					.show();
					getContext(),
					"ERROR: Failed to create GraphicalProductionSet from URL: \""
							+ graphicalProductionSetUrl + "\"", Toast.LENGTH_LONG)
					.show();
		}

		/* Create token set parser */
		tokenSetParser = new TokenSetParser(termSet, gpSet, 0.90f);
        mathTexifier   = new ParseTreeMathTexifier(gpSet, termSet);

		/* Create stringizer and evaluator */
		stringizer = gpSet.genStringizer();
		evaluator = gpSet.genEvaluator();

		String msgText;
		if (isParserReady()) {
//            msgText = "Graphical token-set parser created from: \n";
        } else {
            msgText = "FAILED to create graphical token-set parser from:\n";

            Toast msg = Toast.makeText(getContext(), msgText + "Terminals file: "
                    + terminalSetUrl + "\n" + "Productions file: "
                    + graphicalProductionSetUrl, Toast.LENGTH_SHORT);
            msg.show();
        }

	}

	public void createStrokeCurator(URL configFN, TokenRecogEngine tokenEngine) {
		strokeCurator = new StrokeCuratorConfigurable(configFN, tokenEngine);
	}

	public boolean isParserReady() {
		return (tokenSetParser != null && stringizer != null && evaluator != null);
	}

	public void createWrittenTokenSetDrawer() {
		this.wtSetDrawer = new CWrittenTokenSetDrawer();
	}

	public void setCanvasTokenNames() {
		if (tokEngine.tokenNames != null) {
			String[] aTokenNames = new String[tokEngine.tokenNames.size()];
			tokEngine.tokenNames.toArray(aTokenNames);
			// aTokenNames = (String
			// [])tokEngine.tokenNames.toArray(tokEngine.tokenNames);
			super.setTokenNames(aTokenNames);
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {
		boolean b = super.onTouchEvent(event);

		switch (event.getAction()) {
		case MotionEvent.ACTION_DOWN:
			if (flagWaitTillRecog) {
				strokeRecogFuture.cancel(false);
				flagRecog = false;
				flagWaitTillRecog = false;
			}

			break;
		case MotionEvent.ACTION_MOVE:
			break;
		case MotionEvent.ACTION_UP:
            if ( !this.getHasBeenPinch() &&
                 !this.getHasBeenSelection() &&
                 !this.getHasBeenTokenMove() ) {
                act(PlatoNotesAction.AddStroke);
            }

            /* Handle token movements */
            if (idxTokenMoved != -1) {
                /* Create string argument for idxTokenMoved */
                final String idxTokenMovedStr = String.format("%d", idxTokenMoved);

                /* Create string argument for newBounds */
                StringBuilder newBoundsStrBuilder = new StringBuilder();
                final float[] newBounds = wtSet.getTokenBounds(idxTokenMoved);
                for (int k = 0; k < newBounds.length; ++k) {
                    newBoundsStrBuilder.append(String.format("%f", newBounds[k]));

                    if (k < newBounds.length - 1) {
                        newBoundsStrBuilder.append(",");
                    }
                }

                act(PlatoNotesAction.MoveToken, idxTokenMovedStr, newBoundsStrBuilder.toString());
            }

            this.externalResetUIState();

			break;
		default:
			break;
		}

		return b;
	}

    private String intArray2String(final int[] ints) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < ints.length; ++i) {
            sb.append(Integer.toString(ints[i]));
            if (i < ints.length - 1) {
                sb.append(",");
            }
        }

        return sb.toString();
    }

    public synchronized void cancelParserAsyncTask() {
        // TODO: Cancel web server operations
        if (asyncTask != null) {
            AsyncTask.Status status = asyncTask.getStatus();

            if (status == AsyncTask.Status.PENDING || status == AsyncTask.Status.RUNNING) {
                try {
                    System.out.print("Attempting to cancel parser async task");

                    parsingCancelledDueToTimeOut = false;
                    asyncTask.cancel(false);
                } catch (Exception exc) {

                }
            }
        }
    }

	public synchronized void act(PlatoNotesAction action, String... actionParams) {
		act(false, action, actionParams);
	}

	public synchronized void actAgain() {
		act(true, cachedAction, cachedActionParams);
	}

    public synchronized void act(boolean repeat, PlatoNotesAction action, String... actionParams) {

		if (repeat && action.isPrimarilyLocal()) {
			// Do not repeat primarily-local actions, as the result of the action should have
			// already been synced to server side via injectState()
			return;
		}

		// Store current action and actionParameters, just in case this action needs to be repeated in the
		// face or engine expiration or purging.
		cachedAction       = action;
		cachedActionParams = actionParams;

        if (action == PlatoNotesAction.AddStroke) {
            strokeCurator.addStroke(lastStroke);

            /* Display other token candidates */
            /* TODO: Refactor */
            final int nMax = (tokEngine.tokenNames.size() >= 5) ? 5 : tokEngine.tokenNames.size();
            final Bus eventBus = EventBus.getInstance();
            eventBus.post(new TokenRecogCandidatesEvent(strokeCurator.getTokenSet(),
                                                        strokeCurator.getNumTokens() - 1,
                                                        nMax,
                                                        tokEngine.tokenNames));

            // To improve smoothness of the writing experience, the local token curator is always
            // used, even when web session is available. In other words, when a web session is
            // available, add-stroke events will be dispatched to both the local curator and the web
            // session. The same applies to actions remove-last-token, clear, force-set-token-name,
            // merge-strokes-as-token and unmerge-last-stroke.
            updateWrittenTokenSet();

            if (isWebClientReady()) {
                PlatoWebClientTaskRequest webReq = new PlatoWebClientTaskRequest(webClient, action, lastStroke);
                new AndroidWebClientAsyncTask().execute(webReq);
            }

        } else if (action == PlatoNotesAction.RemoveLastToken) {
            int[] pathRemoveIndices = strokeCurator.removeLastToken();

            if (pathRemoveIndices != null) {
                for (int n = pathRemoveIndices.length - 1; n >= 0; --n) {
                    paths.remove(pathRemoveIndices[n]);
                    nPoints.remove(pathRemoveIndices[n]);
                    firstPoints.remove(pathRemoveIndices[n]);
                }
            }

            updateWrittenTokenSet();

            if (isWebClientReady()) {
                asyncInvokeWebClient(action);
            }
        } else if (action == PlatoNotesAction.ForceSetTokenName) {
            final int tokenIdx;
            final String tokenName;
            if (actionParams.length == 2) {
                tokenIdx = Integer.parseInt(actionParams[0]);
                tokenName = actionParams[1];
            } else if (actionParams.length == 1) {
                /* 2 args: set the last token */
                tokenIdx = wtSet.getNumTokens() - 1;
                tokenName = actionParams[0];
            } else {
                throw new IllegalArgumentException("Unexpected length of actionParams: " + actionParams.length);
            }

            strokeCurator.forceSetRecogWinner(tokenIdx, tokenName);

            updateWrittenTokenSet();

            if (isWebClientReady()) {
                asyncInvokeWebClient(action, Integer.toString(tokenIdx), tokenName);
            }
        } else if (action ==PlatoNotesAction.ClearStrokes) {
            super.clear();
            strokeCurator.clear();

            updateWrittenTokenSet();

            // Local-web coordination
            if (isWebClientReady()) {
                asyncInvokeWebClient(action);
            }
        } else if (action == PlatoNotesAction.MergeStrokesAsToken) {
            int numStrokesToMerge = 2;
            if (actionParams.length > 0) {
                numStrokesToMerge = Integer.parseInt(actionParams[0]);
            }

            final int nStrokes = strokeCurator.getNumStrokes();
            if (nStrokes >= numStrokesToMerge) {
                final int[] strokeIndices = new int[numStrokesToMerge];
                for (int i = 0; i < numStrokesToMerge; ++i) {
                    strokeIndices[i] = nStrokes - (numStrokesToMerge - i);
                }

                strokeCurator.mergeStrokesAsToken(strokeIndices);

                updateWrittenTokenSet();

                if (isWebClientReady()) {
                    asyncInvokeWebClient(action, intArray2String(strokeIndices));
                }
            }
        } else if (action == PlatoNotesAction.UnmergeLastStroke) {
            final int nStrokes = strokeCurator.getNumStrokes();

            if (nStrokes > 1) {
                int[] idxStrokesToUnmerge = new int[] {nStrokes - 1};

                strokeCurator.mergeStrokesAsToken(idxStrokesToUnmerge);

                updateWrittenTokenSet();

                if (isWebClientReady()) {
                    asyncInvokeWebClient(PlatoNotesAction.MergeStrokesAsToken, intArray2String(idxStrokesToUnmerge));
                }
            }
        } else if (action == PlatoNotesAction.MoveToken) {
            if (isWebClientReady()) {
                asyncInvokeWebClient(PlatoNotesAction.MoveToken, actionParams[0], actionParams[1]);
            }
        } else if (action == PlatoNotesAction.ParseTokenSet) {
            if (isWebClientReady()) {
                /* Web parser-evaluator */
                asyncInvokeWebClient(action);
            } else {
                /* Local parser-evaluator */
                LocalParserAsyncTask localParserAsyncTask = new LocalParserAsyncTask();
                asyncTask = localParserAsyncTask;

                // Make copy of final reference for inner class callbacks
                final AsyncTask localParserAsyncTaskRef = localParserAsyncTask;

                localParserAsyncTask.execute();

                parserKiller = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        int counter = 0;
                        long sleepCycleMillis = 250L;

                        while (counter * sleepCycleMillis < parserTimeoutMillis.get("local")) {
                            try {
                                Thread.sleep(sleepCycleMillis);
                            } catch (InterruptedException exc) {
                            }

                            if (Thread.currentThread().isInterrupted()) {
                                System.out.print("parserKiller is interrupted");
                                return;
                            }

                            counter ++;

                        }

                        if (counter * sleepCycleMillis >= parserTimeoutMillis.get("local")) {
                            parsingCancelledDueToTimeOut = true;

                            System.out.println("Calling cancel() on localParserAsyncTask");
                            localParserAsyncTaskRef.cancel(true);
                        }

                    }
                });

                parserKiller.start();
            }
        } else if (action == PlatoNotesAction.GetVarMap) {
            if (isWebClientReady()) {
				asyncInvokeWebClient(action);
            } else {
                PlatoVarMap varMap = evaluator.getVarMap();

                final Bus eventBus = EventBus.getInstance();
                eventBus.post(varMap);
                // Use wrapper class, otherwise the subscriber method cannot be triggered properly

            }
        } else {
            throw new IllegalArgumentException("Unknown action: " + action);
        }
    }

    /* Local parser async task */
    /* TODO: Refactor to reduce duplicate code with Android WebClientAsyncTask */
    /* TODO: Time out */
    /* TODO: Disable all buttons during operation */
    public class LocalParserAsyncTask extends AsyncTask<Void, Void, PlatoClientTaskResponse> {
        private final Bus eventBus = EventBus.getInstance();
        private PlatoClientTaskResponse resp = new PlatoClientTaskResponse();

        /* Constructor */
        public LocalParserAsyncTask() {
            resp.setOriginalAction(PlatoNotesAction.ParseTokenSet);
            resp.setWrittenTokenSet(wtSet);
            resp.setWrittenTokenConstStrokeIdx(wtConstStrokeIdx);
        }

        @Override
        public PlatoClientTaskResponse doInBackground(Void... args) {
            final long startTime = System.currentTimeMillis();
            TokenSetParserOutput parserOutput = parserTokenSet();
            final long elapsedTime = System.currentTimeMillis() - startTime;

            resp.setParserOutput(parserOutput);
            resp.setElapsedTimeMillis(elapsedTime);

            return resp;
        }

        @Override
        public void onPostExecute(PlatoClientTaskResponse resp) {
            /* Get current time and calculate elapsed time */
            eventBus.post(resp);
        }

        @Override
        public void onCancelled() {
            System.out.println("In onCancelled of LocalParserAsyncTask");

            resp.setElapsedTimeMillis(parserTimeoutMillis.get("local"));
            if (parsingCancelledDueToTimeOut) {
                // Parsing cancelled due to timeout
                resp.setErrorMessage("Parser was timed out after " + parserTimeoutMillis.get("local") +
                                     " ms. To allow longer parser operations, you can adjust timeout.");
            } else {
                // Parsing cancelled due to user cancellation
                resp.setErrorMessage("Parsing was cancelled by user.");
                resp.setCancelled(true);
            }

            eventBus.post(resp);

        }
    }

    /* Web-client async tasks */
    private synchronized void asyncInvokeWebClient(PlatoNotesAction action, String... args) {
//        String action = args[0];

        PlatoWebClientTaskRequest req = null;
        if (action == PlatoNotesAction.ForceSetTokenName) {
            final String tokenIdxStr = args[0];
            final int tokenIdx = Integer.parseInt(tokenIdxStr);
            final String tokenName = args[1];

            req = new PlatoWebClientForceSetTokenNameRequest(webClient, action, tokenIdx, tokenName);
        } else if (action == PlatoNotesAction.MergeStrokesAsToken) {
            String strokeIndicesStr = args[0];
            String[] strokeIndicesItems = strokeIndicesStr.split(",");
            int[] strokeIndices = new int[strokeIndicesItems.length];
            for (int i = 0; i < strokeIndices.length; ++i) {
                strokeIndices[i] = Integer.parseInt(strokeIndicesItems[i]);
            }

            req = new PlatoWebClientMergeStrokesRequest(webClient, action, strokeIndices);
        } else if (action == PlatoNotesAction.MoveToken) {
            /* Get token index */
            int idxToken = Integer.parseInt(args[0]);

            /* Get new bounds */
            String newBoundsStr = args[1];
            String[] newBoundsItems = newBoundsStr.split(",");
            float[] newBounds = new float[newBoundsItems.length];
            for (int i = 0; i < newBoundsItems.length; ++i) {
                newBounds[i] = Float.parseFloat(newBoundsItems[i]);
            }

            req = new PlatoWebClientMoveTokenRequest(webClient, action, idxToken, newBounds);
        } else {
            req = new PlatoWebClientTaskRequest(webClient, action);
        }

        AndroidWebClientAsyncTask androidWebClientAsyncTask = new AndroidWebClientAsyncTask();
        asyncTask = androidWebClientAsyncTask;

        androidWebClientAsyncTask.execute(req);
    }

    private void updateWrittenTokenSet() {
        setWrittenTokenSet(strokeCurator.getWrittenTokenSet(),
                           strokeCurator.getWrittenTokenConstStrokeIndices());
        invalidate();

        final Bus eventBus = EventBus.getInstance();
        eventBus.post(new WrittenTokenSetChangedEvent());
    }

    public CWrittenTokenSet getWrittenTokenSet() {
        return wtSet;
    }

	/* Save a written token to file */
	public void saveToken(String letter) {
		CWrittenToken tokenToSave = null;
		String imgFileBase = null;
		boolean resave = false;

		if (writtenToken.isEmpty()) {
			if (lastSavedImageFileBase == null) {
				/* Token is empty and there is not previously saved token */
				if (isEmpty() || letter == null || letter.isEmpty()) {
					Toast msg = Toast.makeText(getContext(),
							                   "Cannot save token because there is none",
							                   Toast.LENGTH_LONG);
					msg.show();
					Log.i(TAG, "No letter");
					return;
				}
			} else {
				/* Token is empty, but there is a previously saved token. Will re-save that token. */
				tokenToSave = lastSavedWrittenToken;
				imgFileBase = lastSavedImageFileBase;
				resave = true;
			}
		} else {
			/* Save a new token */

			/* Put all stroke coordinate data to the [0, 1] interval */
			writtenToken.normalizeAxes();
			tokenToSave = writtenToken;

			/* Get the token file base name */
			String fname = newFileName(letterDir, LETTER_START, IM_FILE_EXT);
			String path = letterDir.endsWith("/") ? letterDir : letterDir + "/";
			imgFileBase = path + fname;

		}

		/* Save the .wt (written token ASCII) file */
		if (bDebug) {
			System.out.println(tokenToSave.toString());
		}

		/* Get the full paths of the actual files to save */
		String wtFileName = imgFileBase + WT_FILE_EXT;
		String imFileName = imgFileBase + IM_FILE_EXT;

		try {
			tokenToSave.writeFile(wtFileName, letter);
		} catch (IOException e) {
			Log.e(TAG, "Error during saving .wt file", e);
		}

		/* Save the .im (image map) file */
		try {
			tokenToSave.writeImgFile(imFileName, letter, WT_W, WT_H);
		} catch (IOException e) {
			Log.e(TAG, "Error during saving .im file", e);
		}

		/* Save the info in case the token needs to be saved again */
		lastSavedImageFileBase = new String(imgFileBase);
		lastSavedWrittenToken = new CWrittenToken(tokenToSave); /* Use copy constructor */

		/* Toast about successful save */
		Toast msg = Toast.makeText(getContext(),
						           String.format((resave ? "Re-saved" : "Saved") + " data to token \"%s\" in file: %s",
								                 letter, imgFileBase),
								   Toast.LENGTH_SHORT);
		msg.show();

		/*
		 * this.entry.downsample(getImagePixels());
		 * this.entry.getSampleData().setLetter(letter.charAt(0));
		 * letterList.add((SampleData) entry.getSampleData().clone());
		 */
		act(PlatoNotesAction.ClearStrokes);
	}

	/* Get the number of valid image files */
	private int[] getNValidImFiles() {
		int[] nvf = new int[letters.length];

		File dir = new File(letterDir);
		final String prefix = LETTER_START;

		File[] files = dir.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				return (name.startsWith(prefix) && name.endsWith(IM_FILE_EXT));
			}
		});

		for (int i = 0; i < files.length; ++i) {
			try {
				FileInputStream fin = new FileInputStream(files[i]);
				BufferedReader in = new BufferedReader(new InputStreamReader(
						fin));
				String line = null;
				String tokenName = null;
				int line_n = 0;
				boolean bValid = true;
				while ((line = in.readLine()) != null) {
					if (line_n == 0) {
						if (line.startsWith("Token name: ")) {
							tokenName = line.replaceFirst("Token name: ", "");
						} else {
							bValid = false;
							break;
						}
					} else if (line_n == 1) {
						if (!line.startsWith("n_w = ")) {
							bValid = false;
							break;
						}
					} else if (line_n == 2) {
						if (!line.startsWith("n_h = ")) {
							bValid = false;
							break;
						}
					} else if (line_n == 3) {
						if (!line.startsWith("w = ")) {
							bValid = false;
							break;
						}
					} else if (line_n == 4) {
						if (!line.startsWith("h = ")) {
							bValid = false;
							break;
						}
					} else if (line_n == 5) {
						break;
					}

					line_n++;
				}
				in.close();

				/* Increment count */
				if (bValid && (tokenName != null)) {
					int k = Arrays.asList(letters).indexOf(tokenName); /*
																		 * TODO:
																		 * Performance
																		 * tweak
																		 * ?
																		 */
					if (k >= 0 && k < nvf.length) {
						nvf[k]++;
					}
				}

			} catch (IOException e) {
				/* TODO */
			}
		}

		return nvf;
	}

	/* Show summary of existing valid data files */
	public void showImFileSummary() {
		int[] nvf = getNValidImFiles();

		String sumStr = new String("Summary of existing valid data files:\n");
		for (int i = 0; i < letters.length; ++i) {
			sumStr += "Letter \"" + letters[i] + "\": " + nvf[i] + " ";
			sumStr += (nvf[i] <= 1) ? "file" : "files";
			sumStr += "\n";
		}

		Toast msg = Toast.makeText(getContext(), sumStr, Toast.LENGTH_LONG);
		msg.show();
	}

	/* Get the name of the new letter or token-set file */
	private String newFileName(String filePath, final String prefix,
			final String extName) {
		File dir = new File(filePath);

		/* Count the number of files in the directory */
//		String [] params = {"/bin/sh", "-c", "ls -f " + filePath + " | wc -l"};
//		int fileCount = -1;
//		try {
//			Process process = Runtime.getRuntime().exec(params);
//			BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
//			fileCount = Integer.parseInt(br.readLine().trim());
//		}
//		catch (IOException ioe) {
//			System.out.println(ioe.getMessage());
//			/* TODO */
//		}
		File[] files = dir.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				// return (name.startsWith(prefix) && name.endsWith(IM_FILE_EXT));
				return (name.startsWith(prefix) && name.endsWith(extName));
			}
		});

		String newName = null;
		int n = files.length + 1;
		boolean availableNameFound = false;
		while (!availableNameFound) {
			newName = prefix + n;
			String newFullPath = filePath +
					          (filePath.endsWith(File.separator) ? "" : File.separator) +
					          newName + extName;
			if (files.length > 0) {
				String lastFilePath = files[files.length - 1].getAbsolutePath();
				if (lastFilePath.equals(newFullPath)) {
					n++;
				}
				else {
					availableNameFound = true;
				}
			}
			else {
				availableNameFound = true;
			}
		}

		return newName;

	}

	private File[] getFiles(String filepath, final String prefix) {
		File dir = new File(filepath);
		// final String prefix = LETTER_START;
		File[] files = dir.listFiles(new FilenameFilter() {
			@Override
			public boolean accept(File dir, String name) {
				return name.startsWith(prefix);
			}
		});
		return files;
	}

	public void deleteLastLetterFile() {
		String[] fnames = getLastLetterFiles(letterDir);

        for (int i = 0; i < fnames.length; ++i) {
            String fname = fnames[i];

            if (fname != null) {
                File file = new File(fname);
                if (file.delete()) {
                    Toast msg = Toast.makeText(getContext(),
                            "File \"" + file.getName() + "\" deleted",
                            Toast.LENGTH_SHORT);
                    msg.show();
                } else {
                    Toast msg = Toast.makeText(getContext(),
                            "Failed to delete \"" + file.getName() + "\"",
                            Toast.LENGTH_SHORT);
                    msg.show();
                }
            }
        }
	}

	private String[] getLastLetterFiles(String filepath) {
		File[] files = getFiles(filepath, LETTER_START);
		if (files.length == 0) {
			return null;
		}
		Integer filenos[] = new Integer[files.length];
		int i = 0;
		for (File file : files) {
			String name = file.getName();
			int no;
			try {
				no = Integer.parseInt(name.substring(name.lastIndexOf("_") + 1, name.lastIndexOf(".")));
			} catch (Exception e) {
				no = 0;
			}
			filenos[i++] = no;
		}
		Arrays.sort(filenos);
		String fname = LETTER_START + (filenos[filenos.length - 1]);

        String[] fileNames = new String[2];

		fileNames[0] = (filepath.endsWith("/") ? filepath : filepath + "/") + fname + WT_FILE_EXT;
        fileNames[1] = (filepath.endsWith("/") ? filepath : filepath + "/") + fname + IM_FILE_EXT;

        return fileNames;
	}

	/* Re-format all .im files */
	public void reFormatImFiles() {
		tokEngine.reFormatImFiles(letterDir, WT_W, WT_H);
	}

	/* Train token recognition engine */
	// public void trainTokenEngine(ProgressDialog progDialog) { /* TODO: Remove
	// local training on Android */
	// tokEngine.setTrainingProgDialog(progDialog);
	// tokEngine.train(letterDir);
	//
	// setCanvasTokenNames();
	// // strokeCurator.initialize(tokenNames, tokEngine);
	// strokeCurator.initialize(tokEngine);
	//
	// /* Write token engine to FS */
	// writeTokenEngine(tokEngine);
	// }

	private String getTokenEngineFileName() {
		String path = engineDir.endsWith("/") ? engineDir : engineDir + "/";

		String sz = "sz" + (getIncludeTokenSize() ? "1" : "0");
		String whr = "whr" + (getIncludeTokenWHRatio() ? "1" : "0");
		String ns = "ns" + (getIncludeTokenNumStrokes() ? "1" : "0");

		String tokenEngineFN = "token_engine." + tokenEngineType.toLowerCase()
				+ "." + sz + "_" + whr + "_" + ns + ".ser";
		tokenEngineFN = path + tokenEngineFN;

		return tokenEngineFN;
	}

	/* Write token engine to file system */
	public void writeTokenEngine(TokenRecogEngine te) {
		/* Get the file name */
		String tokenEngineFN = getTokenEngineFileName();

		/* Write the engine to disk, which is serializable */
		ObjectOutputStream objOutStream = null;
		try {
			objOutStream = new ObjectOutputStream(new FileOutputStream(
					tokenEngineFN));
			objOutStream.writeObject(te);
			objOutStream.flush();
		} catch (IOException e) {
			Log.e("WRITE_TOKEN_ENGINE_ERROR",
					"Failed to write token engine to file: " + tokenEngineFN);
		} finally {
			try {
				objOutStream.close();
			} catch (IOException e) {
				Log.e("TOKEN_ENGINE_OUTPUT_STREAM_CLOSE_ERR",
						"Failed to close token engine output stream");
			}
		}
	}

	/* Load trained token engine */
	public void readTokenEngine(final URL tokenEngineFileUrl) {

		ObjectInputStream objInStream = null;
		boolean readSuccessful = false;
		try {

            System.out.println("readTokenEngine(): Instantiating ObjectInputStream from " + tokenEngineFileUrl);
			objInStream = new ObjectInputStream(new BufferedInputStream(tokenEngineFileUrl.openStream()));

            System.out.println("readTokenEngine(): Calling readObject()");
			if (tokenEngineType.equals("SDV")) {
				tokEngine = (TokenRecogEngineSDV) objInStream.readObject();
			} else if (tokenEngineType.equals("IMG")) {
				tokEngine = (TokenRecogEngineIMG) objInStream.readObject();
			} else {
				throw new RuntimeException("Unrecognized token engine type: "
						+ tokenEngineType);
			}
            System.out.println("readTokenEngine(): DONE Calling readObject()");

			readSuccessful = true;
			// objInStream.close();
		} catch (IOException e) {
			Log.e("READ_TOKEN_ENGINE_ERROR",
					"Failed to read token engine from URL: " + tokenEngineFileUrl);
			Toast msg = Toast.makeText(getContext(),
					"IO error occurred during loading of token engine at "
							+ tokenEngineFileUrl, Toast.LENGTH_LONG);
			msg.show();
		} catch (ClassNotFoundException e) {
			Log.e("READ_TOKEN_ENGINE_ERROR",
					"Failed to read token engine from URL: " + tokenEngineFileUrl);
			Toast msg = Toast.makeText(getContext(),
					"Class error occurred during loading of token engine at "
							+ tokenEngineFileUrl, Toast.LENGTH_SHORT);
			msg.show();
		} finally {
			try {
                System.out.println("readTokenEngine(): Calling objInStream.close()");
				objInStream.close();
			} catch (IOException e) {
				Log.e("INPUT_STREAM_CLOSE_ERR",
						"Failed to close token engine input stream");
			}
		}

        System.out.println("readTokenEngine(): Calling setTokenEngineFeatures()");
		setTokenEngineFeatures();
        System.out.println("readTokenEngine(): DONE Calling setTokenEngineFeatures()");

		if (!readSuccessful) {
			Toast msg = Toast.makeText(
					getContext(),
					"ERROR: Failed to Loaded token engine from URL: " + tokenEngineFileUrl
							+ ".\nmaxIter=" + tokEngine.getTrainMaxIter()
							+ ";\nthreshErr="
							+ tokEngine.getTrainThreshErrRate(),
					Toast.LENGTH_LONG);
			msg.show();
		}
	}

	/* Get the list of unique token names */
	public ArrayList<String> getTokenNames() {
		return tokEngine.tokenNames;
	}

	class ClearProbValsTask implements Runnable {
		public void run() {
			clearProbabilityValues();
			postInvalidate();
		}
	}

	/* Check if the token engine is ready to perform recognition */
	public boolean isReadyToRecognize() {
		return tokEngine.isReadyToRecognize();
	}

	class AlertPrompt {
		String letter;

		AlertPrompt(Context context) {
			final EditText input = new EditText(context);
			new AlertDialog.Builder(context)
					.setTitle("Add Letter")
					// .setMessage()
					.setView(input)
					.setPositiveButton("Ok",
							new DialogInterface.OnClickListener() {
								public void onClick(DialogInterface dialog,
										int whichButton) {
									letter = input.getText().toString();
									Log.i(TAG, "onClick Letter" + letter);
								}
							})
					.setNegativeButton("Cancel",
							new DialogInterface.OnClickListener() {
								public void onClick(DialogInterface dialog,
										int whichButton) {
									// Do nothing.
								}
							}).show();
		}

		public String getLetter() {
			return letter;
		}
	}

	/* Write token set to file */
	public void saveTokenSet(String wtsFileName, boolean bBrief, boolean bPrompt)
			throws IOException {
		try {
			wtSet.writeToFileBrief(wtsFileName, bBrief);
		} catch (IOException e) {
			Toast msg = Toast.makeText(getContext(),
					"IOException occurred during writing to token-set file: "
							+ wtsFileName, Toast.LENGTH_LONG);
			msg.show();
		} catch (IllegalStateException e) {
			Toast msg = Toast.makeText(getContext(),
					"IllegalStateException occurred during writing to token-set file: "
							+ wtsFileName, Toast.LENGTH_LONG);
			msg.show();
		}

		if (bPrompt) {
			Toast msg = Toast.makeText(getContext(),
					"Saved token set to file: " + wtsFileName,
					Toast.LENGTH_LONG);
			msg.show();
		}

		act(PlatoNotesAction.ClearStrokes);
	}

	public void saveTokenSet(boolean bBrief, boolean bPrompt)
			throws IOException {
        tokenSetDir = FileSystemHelper.ensureExternalDirectory(getContext(), TOKENSETDIR);

		/* Directory check */
//		externalDir = getContext().getExternalFilesDir(null).getPath();
//		tokenSetDir = externalDir.endsWith("/") ? externalDir : externalDir
//				+ "/";
//		tokenSetDir += TOKENSETDIR;
//
//		File tsDir = new File(tokenSetDir);
//		if (!tsDir.isDirectory()) {
//			boolean createDirRes = tsDir.mkdir();
//
//			if (!createDirRes)
//				throw new IOException(
//						"Failed to create directory for token sets: "
//								+ tokenSetDir);
//		}

		/* Determine the file name of the new .wts file */
		final String extName = ".wts";
		String fName = newFileName(tokenSetDir, TOKENSET_START, extName);
		String path = tokenSetDir.endsWith("/") ? tokenSetDir : tokenSetDir
				+ "/";
		String wtsFileName = path + fName + extName;

		saveTokenSet(wtsFileName, bBrief, bPrompt);
	}

//    public TextView getWebClientStatusDisplay() {
//        return webClientStatusDisplay;
//    }
//
//    public void setWebClientStatusDisplay(TextView webClientStatusDisplay) {
//        this.webClientStatusDisplay = webClientStatusDisplay;
//    }

    public void setBaseContext(Context baseContext) {
        this.baseContext = baseContext;
    }

    @Override
    public void onSizeChanged(int w, int h, int oldw, int oldh) {
        this.wtSetDrawer.setScreenWH((float) w, (float) h);

    }

    public void resetBoardZoomPan() {
        this.wtSetDrawer.reset();
    }

    public TokenSetParserOutput parserTokenSet() {
        if (!isParserReady()) {
            throw new RuntimeException("[ERROR: Parser not ready]");
        } else if (wtSet.empty()) {
            throw new RuntimeException("[WARNING: Empty token set]");
        } else {

            CWrittenTokenSetNoStroke wtSetNS = new CWrittenTokenSetNoStroke(wtSet);
            Node parseRoot = null;
            try {
                parseRoot = tokenSetParser.parse(wtSetNS);
            } catch (TokenSetParserException exc) {

            }
            String stringized = stringizer.stringize(parseRoot);

            String evalResStr = "";
            String mathTex = "";
//            if (parseRoot != null) {
                try {
                    evalResStr = evaluator.eval2String(parseRoot); /* TODO: Not all evaluation output are doubles */
                }
                catch (Exception exc) {
                    String rootCauseMsg = ExceptionUtils.getRootCauseMessage(exc);
                    evalResStr = "[Evaluator exception: " + rootCauseMsg + "]";
                }

                mathTex = mathTexifier.texify(parseRoot);
//            }

            TokenSetParserOutput parserOutput = new TokenSetParserOutput(stringized, evalResStr, mathTex);

            return parserOutput;
        }
    }

    public void stopParserKiller() {
        if (parserKiller != null) {
            parserKiller.interrupt();
        }
    }

    public void cancelWebClient() {
        if (webClient != null) {
            asyncInvokeWebClient(PlatoNotesAction.RemoveEngine);

        }
    }

    public void setDisplayOverlaidSymbols(boolean displayOverlaidSymbols) {
        wtSetDrawer.setDisplayOverlaidSymbols(displayOverlaidSymbols);
    }

}


text:
package com.mbrlabs.mundus.editor.scene3d.components;

import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.mbrlabs.mundus.commons.scene3d.GameObject;
import com.mbrlabs.mundus.commons.scene3d.components.Component;
import com.mbrlabs.mundus.commons.scene3d.components.ModelComponent;
import com.mbrlabs.mundus.editor.shader.Shaders;
import com.mbrlabs.mundus.editor.tools.picker.PickerColorEncoder;
import com.mbrlabs.mundus.editor.tools.picker.PickerIDAttribute;

/**
 * @author Marcus Brummer
 * @version 27-10-2016
 */
public class PickableModelComponent extends ModelComponent implements PickableComponent {

    public PickableModelComponent(GameObject go, Shader shader) {
        super(go, shader);
    }

    @Override
    public void encodeRaypickColorId() {
        PickerIDAttribute goIDa = PickerColorEncoder.encodeRaypickColorId(gameObject);
        this.modelInstance.materials.first().set(goIDa);
    }

    @Override
    public void renderPick() {
        gameObject.sceneGraph.batch.render(modelInstance, Shaders.INSTANCE.getPickerShader());
    }

    @Override
    public Component clone(GameObject go) {
        PickableModelComponent mc = new PickableModelComponent(go, shader);
        mc.modelAsset = this.modelAsset;
        mc.modelInstance = new ModelInstance(modelAsset.getModel());
        mc.shader = this.shader;
        mc.encodeRaypickColorId();
        return mc;
    }
}

redpajama_set_name: RedPajamaGithub


text:
<?php

if (class_exists('User') != true) {

    class User extends \Trucker\Resource\Model
    {

    }
}


text:
#include "SkCanvas.h"
#include "SkData.h"
#include "SkDevice.h"
#include "SkPaint.h"
#include "SkPathEffect.h"
#include "SkGPipe.h"
#include "SkGPipePriv.h"
#include "SkImageFilter.h"
#include "SkStream.h"
#include "SkTSearch.h"
#include "SkTypeface.h"
#include "SkWriter32.h"
#include "SkColorFilter.h"
#include "SkDrawLooper.h"
#include "SkMaskFilter.h"
#include "SkRasterizer.h"
#include "SkShader.h"
#include "SkOrderedWriteBuffer.h"

static SkFlattenable* get_paintflat(const SkPaint& paint, unsigned paintFlat) {
    SkASSERT(paintFlat < kCount_PaintFlats);
    switch (paintFlat) {
        case kColorFilter_PaintFlat:    return paint.getColorFilter();
        case kDrawLooper_PaintFlat:     return paint.getLooper();
        case kMaskFilter_PaintFlat:     return paint.getMaskFilter();
        case kPathEffect_PaintFlat:     return paint.getPathEffect();
        case kRasterizer_PaintFlat:     return paint.getRasterizer();
        case kShader_PaintFlat:         return paint.getShader();
        case kImageFilter_PaintFlat:    return paint.getImageFilter();
        case kXfermode_PaintFlat:       return paint.getXfermode();
    }
    SkDEBUGFAIL("never gets here");
    return NULL;
}

static size_t estimateFlattenSize(const SkPath& path) {
    int n = path.countPoints();
    size_t bytes = 3 * sizeof(int32_t);
    bytes += n * sizeof(SkPoint);
    bytes += SkAlign4(n + 2);    // verbs: add 2 for move/close extras

#ifdef SK_DEBUG
    {
        SkWriter32 writer(1024);
        path.flatten(writer);
        SkASSERT(writer.size() <= bytes);
    }
#endif
    return bytes;
}

static size_t writeTypeface(SkWriter32* writer, SkTypeface* typeface) {
    SkASSERT(typeface);
    SkDynamicMemoryWStream stream;
    typeface->serialize(&stream);
    size_t size = stream.getOffset();
    if (writer) {
        writer->write32(size);
        SkAutoDataUnref data(stream.copyToData());
        writer->write(data.data(), size);
    }
    return 4 + size;
}

///////////////////////////////////////////////////////////////////////////////

class SkGPipeCanvas : public SkCanvas {
public:
    SkGPipeCanvas(SkGPipeController*, SkWriter32*, SkFactorySet*);
    virtual ~SkGPipeCanvas();

    void finish() {
        if (!fDone) {
            if (this->needOpBytes()) {
                this->writeOp(kDone_DrawOp);
                this->doNotify();
            }
            fDone = true;
        }
    }

    // overrides from SkCanvas
    virtual int save(SaveFlags) SK_OVERRIDE;
    virtual int saveLayer(const SkRect* bounds, const SkPaint*,
                          SaveFlags) SK_OVERRIDE;
    virtual void restore() SK_OVERRIDE;
    virtual bool translate(SkScalar dx, SkScalar dy) SK_OVERRIDE;
    virtual bool scale(SkScalar sx, SkScalar sy) SK_OVERRIDE;
    virtual bool rotate(SkScalar degrees) SK_OVERRIDE;
    virtual bool skew(SkScalar sx, SkScalar sy) SK_OVERRIDE;
    virtual bool concat(const SkMatrix& matrix) SK_OVERRIDE;
    virtual void setMatrix(const SkMatrix& matrix) SK_OVERRIDE;
    virtual bool clipRect(const SkRect& rect, SkRegion::Op op,
                          bool doAntiAlias = false) SK_OVERRIDE;
    virtual bool clipPath(const SkPath& path, SkRegion::Op op,
                          bool doAntiAlias = false) SK_OVERRIDE;
    virtual bool clipRegion(const SkRegion& region, SkRegion::Op op) SK_OVERRIDE;
    virtual void clear(SkColor) SK_OVERRIDE;
    virtual void drawPaint(const SkPaint& paint) SK_OVERRIDE;
    virtual void drawPoints(PointMode, size_t count, const SkPoint pts[],
                            const SkPaint&) SK_OVERRIDE;
    virtual void drawRect(const SkRect& rect, const SkPaint&) SK_OVERRIDE;
    virtual void drawPath(const SkPath& path, const SkPaint&) SK_OVERRIDE;
    virtual void drawBitmap(const SkBitmap&, SkScalar left, SkScalar top,
                            const SkPaint*) SK_OVERRIDE;
    virtual void drawBitmapRect(const SkBitmap&, const SkIRect* src,
                                const SkRect& dst, const SkPaint*) SK_OVERRIDE;
    virtual void drawBitmapMatrix(const SkBitmap&, const SkMatrix&,
                                  const SkPaint*) SK_OVERRIDE;
    virtual void drawBitmapNine(const SkBitmap& bitmap, const SkIRect& center,
                                const SkRect& dst, const SkPaint* paint = NULL) SK_OVERRIDE;
    virtual void drawSprite(const SkBitmap&, int left, int top,
                            const SkPaint*) SK_OVERRIDE;
    virtual void drawText(const void* text, size_t byteLength, SkScalar x,
                          SkScalar y, const SkPaint&) SK_OVERRIDE;
    virtual void drawPosText(const void* text, size_t byteLength,
                             const SkPoint pos[], const SkPaint&) SK_OVERRIDE;
    virtual void drawPosTextH(const void* text, size_t byteLength,
                              const SkScalar xpos[], SkScalar constY,
                              const SkPaint&) SK_OVERRIDE;
    virtual void drawTextOnPath(const void* text, size_t byteLength,
                            const SkPath& path, const SkMatrix* matrix,
                                const SkPaint&) SK_OVERRIDE;
    virtual void drawPicture(SkPicture& picture) SK_OVERRIDE;
    virtual void drawVertices(VertexMode, int vertexCount,
                          const SkPoint vertices[], const SkPoint texs[],
                          const SkColor colors[], SkXfermode*,
                          const uint16_t indices[], int indexCount,
                              const SkPaint&) SK_OVERRIDE;
    virtual void drawData(const void*, size_t) SK_OVERRIDE;

private:
    SkFactorySet* fFactorySet;  // optional, only used if cross-process
    SkGPipeController* fController;
    SkWriter32& fWriter;
    size_t      fBlockSize; // amount allocated for writer
    size_t      fBytesNotified;
    bool        fDone;

    SkRefCntSet fTypefaceSet;

    uint32_t getTypefaceID(SkTypeface*);

    inline void writeOp(DrawOps op, unsigned flags, unsigned data) {
        fWriter.write32(DrawOp_packOpFlagData(op, flags, data));
    }

    inline void writeOp(DrawOps op) {
        fWriter.write32(DrawOp_packOpFlagData(op, 0, 0));
    }

    bool needOpBytes(size_t size = 0);

    inline void doNotify() {
        if (!fDone) {
            size_t bytes = fWriter.size() - fBytesNotified;
            if (bytes > 0) {
                fController->notifyWritten(bytes);
                fBytesNotified += bytes;
            }
        }
    }

    struct FlatData {
        uint32_t    fIndex; // always > 0
        uint32_t    fSize;

        void*       data() { return (char*)this + sizeof(*this); }

        static int Compare(const FlatData* a, const FlatData* b) {
            return memcmp(&a->fSize, &b->fSize, a->fSize + sizeof(a->fSize));
        }
    };

    SkTDArray<FlatData*> fBitmapArray;
    int flattenToIndex(const SkBitmap&);

    SkTDArray<FlatData*> fFlatArray;
    int fCurrFlatIndex[kCount_PaintFlats];
    int flattenToIndex(SkFlattenable* obj, PaintFlats);

    SkPaint fPaint;
    void writePaint(const SkPaint&);

    class AutoPipeNotify {
    public:
        AutoPipeNotify(SkGPipeCanvas* canvas) : fCanvas(canvas) {}
        ~AutoPipeNotify() { fCanvas->doNotify(); }
    private:
        SkGPipeCanvas* fCanvas;
    };
    friend class AutoPipeNotify;

    typedef SkCanvas INHERITED;
};

int SkGPipeCanvas::flattenToIndex(const SkBitmap & bitmap) {
    SkOrderedWriteBuffer tmpWriter(1024);
    // FIXME: Rather than forcing CrossProcess, we should create an SkRefCntSet
    // so that we can store a pointer to a bitmap's pixels during flattening.
    tmpWriter.setFlags(SkFlattenableWriteBuffer::kCrossProcess_Flag);
    bitmap.flatten(tmpWriter);

    size_t len = tmpWriter.size();
    size_t allocSize = len + sizeof(FlatData);

    SkAutoSMalloc<1024> storage(allocSize);
    FlatData* flat = (FlatData*)storage.get();
    flat->fSize = len;
    tmpWriter.flatten(flat->data());

    int index = SkTSearch<FlatData>((const FlatData**)fBitmapArray.begin(),
                                     fBitmapArray.count(), flat, sizeof(flat),
                                     &FlatData::Compare);
    if (index < 0) {
        index = ~index;
        FlatData* copy = (FlatData*)sk_malloc_throw(allocSize);
        memcpy(copy, flat, allocSize);
        // For bitmaps, we can use zero based indices, since we will never ask
        // for a NULL bitmap (unlike with paint flattenables).
        copy->fIndex = fBitmapArray.count();
        *fBitmapArray.insert(index) = copy;
        if (this->needOpBytes(len + sizeof(uint32_t))) {
            this->writeOp(kDef_Bitmap_DrawOp, 0, copy->fIndex);
            fWriter.write32(len);
            fWriter.write(copy->data(), len);
        }
    }
    return fBitmapArray[index]->fIndex;
}

// return 0 for NULL (or unflattenable obj), or index-base-1
int SkGPipeCanvas::flattenToIndex(SkFlattenable* obj, PaintFlats paintflat) {
    if (NULL == obj) {
        return 0;
    }

    SkOrderedWriteBuffer tmpWriter(1024);

    // Needs to be cross process so a bitmap shader will be preserved
    // FIXME: Rather than forcing CrossProcess, we should create an SkRefCntSet
    // so that we can store a pointer to a bitmap's pixels during flattening.
    tmpWriter.setFlags((SkFlattenableWriteBuffer::Flags)
                       (SkFlattenableWriteBuffer::kInlineFactoryNames_Flag
                       | SkFlattenableWriteBuffer::kCrossProcess_Flag));
    tmpWriter.setFactoryRecorder(fFactorySet);

    tmpWriter.writeFlattenable(obj);
    size_t len = tmpWriter.size();
    size_t allocSize = len + sizeof(FlatData);

    SkAutoSMalloc<1024> storage(allocSize);
    FlatData* flat = (FlatData*)storage.get();
    flat->fSize = len;
    tmpWriter.flatten(flat->data());

    int index = SkTSearch<FlatData>((const FlatData**)fFlatArray.begin(),
                                    fFlatArray.count(), flat, sizeof(flat),
                                    &FlatData::Compare);
    if (index < 0) {
        index = ~index;
        FlatData* copy = (FlatData*)sk_malloc_throw(allocSize);
        memcpy(copy, flat, allocSize);
        *fFlatArray.insert(index) = copy;
        // call this after the insert, so that count() will have been grown
        copy->fIndex = fFlatArray.count();
//        SkDebugf("--- add flattenable[%d] size=%d index=%d\n", paintflat, len, copy->fIndex);

        if (this->needOpBytes(len)) {
            this->writeOp(kDef_Flattenable_DrawOp, paintflat, copy->fIndex);
            fWriter.write(copy->data(), len);
        }
    }
    return fFlatArray[index]->fIndex;
}

///////////////////////////////////////////////////////////////////////////////

#define MIN_BLOCK_SIZE  (16 * 1024)

SkGPipeCanvas::SkGPipeCanvas(SkGPipeController* controller,
                             SkWriter32* writer, SkFactorySet* fset)
        : fWriter(*writer) {
    fFactorySet = fset;
    fController = controller;
    fDone = false;
    fBlockSize = 0; // need first block from controller
    fBytesNotified = 0;
    sk_bzero(fCurrFlatIndex, sizeof(fCurrFlatIndex));

    // we need a device to limit our clip
    // should the caller give us the bounds?
    // We don't allocate pixels for the bitmap
    SkBitmap bitmap;
    bitmap.setConfig(SkBitmap::kARGB_8888_Config, 32767, 32767);
    SkDevice* device = SkNEW_ARGS(SkDevice, (bitmap));
    this->setDevice(device)->unref();
}

SkGPipeCanvas::~SkGPipeCanvas() {
    this->finish();

    fFlatArray.freeAll();
    fBitmapArray.freeAll();
}

bool SkGPipeCanvas::needOpBytes(size_t needed) {
    if (fDone) {
        return false;
    }

    needed += 4;  // size of DrawOp atom
    if (fWriter.size() + needed > fBlockSize) {
        // Before we wipe out any data that has already been written, read it
        // out.
        this->doNotify();
        size_t blockSize = SkMax32(MIN_BLOCK_SIZE, needed);
        void* block = fController->requestBlock(blockSize, &fBlockSize);
        if (NULL == block) {
            fDone = true;
            return false;
        }
        fWriter.reset(block, fBlockSize);
        fBytesNotified = 0;
    }
    return true;
}

uint32_t SkGPipeCanvas::getTypefaceID(SkTypeface* face) {
    uint32_t id = 0; // 0 means default/null typeface
    if (face) {
        id = fTypefaceSet.find(face);
        if (0 == id) {
            id = fTypefaceSet.add(face);
            size_t size = writeTypeface(NULL, face);
            if (this->needOpBytes(size)) {
                this->writeOp(kDef_Typeface_DrawOp);
                writeTypeface(&fWriter, face);
            }
        }
    }
    return id;
}

///////////////////////////////////////////////////////////////////////////////

#define NOTIFY_SETUP(canvas)    \
    AutoPipeNotify apn(canvas)

int SkGPipeCanvas::save(SaveFlags flags) {
    NOTIFY_SETUP(this);
    if (this->needOpBytes()) {
        this->writeOp(kSave_DrawOp, 0, flags);
    }
    return this->INHERITED::save(flags);
}

int SkGPipeCanvas::saveLayer(const SkRect* bounds, const SkPaint* paint,
                             SaveFlags saveFlags) {
    NOTIFY_SETUP(this);
    size_t size = 0;
    unsigned opFlags = 0;

    if (bounds) {
        opFlags |= kSaveLayer_HasBounds_DrawOpFlag;
        size += sizeof(SkRect);
    }
    if (paint) {
        opFlags |= kSaveLayer_HasPaint_DrawOpFlag;
        this->writePaint(*paint);
    }

    if (this->needOpBytes(size)) {
        this->writeOp(kSaveLayer_DrawOp, opFlags, saveFlags);
        if (bounds) {
            fWriter.writeRect(*bounds);
        }
    }

    // we just pass on the save, so we don't create a layer
    return this->INHERITED::save(saveFlags);
}

void SkGPipeCanvas::restore() {
    NOTIFY_SETUP(this);
    if (this->needOpBytes()) {
        this->writeOp(kRestore_DrawOp);
    }
    this->INHERITED::restore();
}

bool SkGPipeCanvas::translate(SkScalar dx, SkScalar dy) {
    if (dx || dy) {
        NOTIFY_SETUP(this);
        if (this->needOpBytes(2 * sizeof(SkScalar))) {
            this->writeOp(kTranslate_DrawOp);
            fWriter.writeScalar(dx);
            fWriter.writeScalar(dy);
        }
    }
    return this->INHERITED::translate(dx, dy);
}

bool SkGPipeCanvas::scale(SkScalar sx, SkScalar sy) {
    if (sx || sy) {
        NOTIFY_SETUP(this);
        if (this->needOpBytes(2 * sizeof(SkScalar))) {
            this->writeOp(kScale_DrawOp);
            fWriter.writeScalar(sx);
            fWriter.writeScalar(sy);
        }
    }
    return this->INHERITED::scale(sx, sy);
}

bool SkGPipeCanvas::rotate(SkScalar degrees) {
    if (degrees) {
        NOTIFY_SETUP(this);
        if (this->needOpBytes(sizeof(SkScalar))) {
            this->writeOp(kRotate_DrawOp);
            fWriter.writeScalar(degrees);
        }
    }
    return this->INHERITED::rotate(degrees);
}

bool SkGPipeCanvas::skew(SkScalar sx, SkScalar sy) {
    if (sx || sy) {
        NOTIFY_SETUP(this);
        if (this->needOpBytes(2 * sizeof(SkScalar))) {
            this->writeOp(kSkew_DrawOp);
            fWriter.writeScalar(sx);
            fWriter.writeScalar(sy);
        }
    }
    return this->INHERITED::skew(sx, sy);
}

bool SkGPipeCanvas::concat(const SkMatrix& matrix) {
    if (!matrix.isIdentity()) {
        NOTIFY_SETUP(this);
        if (this->needOpBytes(matrix.flatten(NULL))) {
            this->writeOp(kConcat_DrawOp);
            fWriter.writeMatrix(matrix);
        }
    }
    return this->INHERITED::concat(matrix);
}

void SkGPipeCanvas::setMatrix(const SkMatrix& matrix) {
    NOTIFY_SETUP(this);
    if (this->needOpBytes(matrix.flatten(NULL))) {
        this->writeOp(kSetMatrix_DrawOp);
        fWriter.writeMatrix(matrix);
    }
    this->INHERITED::setMatrix(matrix);
}

bool SkGPipeCanvas::clipRect(const SkRect& rect, SkRegion::Op rgnOp,
                             bool doAntiAlias) {
    NOTIFY_SETUP(this);
    if (this->needOpBytes(sizeof(SkRect)) + sizeof(bool)) {
        this->writeOp(kClipRect_DrawOp, 0, rgnOp);
        fWriter.writeRect(rect);
        fWriter.writeBool(doAntiAlias);
    }
    return this->INHERITED::clipRect(rect, rgnOp, doAntiAlias);
}

bool SkGPipeCanvas::clipPath(const SkPath& path, SkRegion::Op rgnOp,
                             bool doAntiAlias) {
    NOTIFY_SETUP(this);
    if (this->needOpBytes(estimateFlattenSize(path)) + sizeof(bool)) {
        this->writeOp(kClipPath_DrawOp, 0, rgnOp);
        path.flatten(fWriter);
        fWriter.writeBool(doAntiAlias);
    }
    // we just pass on the bounds of the path
    return this->INHERITED::clipRect(path.getBounds(), rgnOp, doAntiAlias);
}

bool SkGPipeCanvas::clipRegion(const SkRegion& region, SkRegion::Op rgnOp) {
    NOTIFY_SETUP(this);
    if (this->needOpBytes(region.flatten(NULL))) {
        this->writeOp(kClipRegion_DrawOp, 0, rgnOp);
        fWriter.writeRegion(region);
    }
    return this->INHERITED::clipRegion(region, rgnOp);
}

///////////////////////////////////////////////////////////////////////////////

void SkGPipeCanvas::clear(SkColor color) {
    NOTIFY_SETUP(this);
    unsigned flags = 0;
    if (color) {
        flags |= kClear_HasColor_DrawOpFlag;
    }
    if (this->needOpBytes(sizeof(SkColor))) {
        this->writeOp(kDrawClear_DrawOp, flags, 0);
        if (color) {
            fWriter.write32(color);
        }
    }
}

void SkGPipeCanvas::drawPaint(const SkPaint& paint) {
    NOTIFY_SETUP(this);
    this->writePaint(paint);
    if (this->needOpBytes()) {
        this->writeOp(kDrawPaint_DrawOp);
    }
}

void SkGPipeCanvas::drawPoints(PointMode mode, size_t count,
                                   const SkPoint pts[], const SkPaint& paint) {
    if (count) {
        NOTIFY_SETUP(this);
        this->writePaint(paint);
        if (this->needOpBytes(4 + count * sizeof(SkPoint))) {
            this->writeOp(kDrawPoints_DrawOp, mode, 0);
            fWriter.write32(count);
            fWriter.write(pts, count * sizeof(SkPoint));
        }
    }
}

void SkGPipeCanvas::drawRect(const SkRect& rect, const SkPaint& paint) {
    NOTIFY_SETUP(this);
    this->writePaint(paint);
    if (this->needOpBytes(sizeof(SkRect))) {
        this->writeOp(kDrawRect_DrawOp);
        fWriter.writeRect(rect);
    }
}

void SkGPipeCanvas::drawPath(const SkPath& path, const SkPaint& paint) {
    NOTIFY_SETUP(this);
    this->writePaint(paint);
    if (this->needOpBytes(estimateFlattenSize(path))) {
        this->writeOp(kDrawPath_DrawOp);
        path.flatten(fWriter);
    }
}

void SkGPipeCanvas::drawBitmap(const SkBitmap& bm, SkScalar left, SkScalar top,
                                   const SkPaint* paint) {
    // This is the brute-force solution
    // TODO: add the notion of a shared, counted for large immutable resources
    NOTIFY_SETUP(this);
    if (paint) {
        this->writePaint(*paint);
    }

    int bitmapIndex = this->flattenToIndex(bm);

    if (this->needOpBytes(sizeof(SkScalar) * 2 + sizeof(bool))) {
        this->writeOp(kDrawBitmap_DrawOp, 0, bitmapIndex);
        fWriter.writeBool(paint != NULL);
        fWriter.writeScalar(left);
        fWriter.writeScalar(top);
    }
}

void SkGPipeCanvas::drawBitmapRect(const SkBitmap& bm, const SkIRect* src,
                                       const SkRect& dst, const SkPaint* paint) {
    NOTIFY_SETUP(this);
    if (paint) {
        this->writePaint(*paint);
    }

    int bitmapIndex = this->flattenToIndex(bm);

    size_t opBytesNeeded = sizeof(SkRect) + sizeof(bool) * 2;
    bool hasSrc = src != NULL;
    if (hasSrc) {
        opBytesNeeded += sizeof(int32_t) * 4;
    }
    if (this->needOpBytes(opBytesNeeded)) {
        this->writeOp(kDrawBitmapRect_DrawOp, 0, bitmapIndex);
        fWriter.writeBool(paint != NULL);
        fWriter.writeBool(hasSrc);
        if (hasSrc) {
            fWriter.write32(src->fLeft);
            fWriter.write32(src->fTop);
            fWriter.write32(src->fRight);
            fWriter.write32(src->fBottom);
        }
        fWriter.writeRect(dst);
    }
}

void SkGPipeCanvas::drawBitmapMatrix(const SkBitmap&, const SkMatrix&,
                                         const SkPaint*) {
    UNIMPLEMENTED
}

void SkGPipeCanvas::drawBitmapNine(const SkBitmap& bm, const SkIRect& center,
                                   const SkRect& dst, const SkPaint* paint) {
    NOTIFY_SETUP(this);
    if (paint) {
        this->writePaint(*paint);
    }
    int bitmapIndex = this->flattenToIndex(bm);

    if (this->needOpBytes(sizeof(int32_t) * 4 + sizeof(bool)
                          + sizeof(SkRect))) {
        this->writeOp(kDrawBitmapNine_DrawOp, 0, bitmapIndex);
        fWriter.writeBool(paint != NULL);
        fWriter.write32(center.fLeft);
        fWriter.write32(center.fTop);
        fWriter.write32(center.fRight);
        fWriter.write32(center.fBottom);
        fWriter.writeRect(dst);
    }
}

void SkGPipeCanvas::drawSprite(const SkBitmap& bm, int left, int top,
                                   const SkPaint* paint) {
    NOTIFY_SETUP(this);
    if (paint) {
        this->writePaint(*paint);
    }
    int bitmapIndex = this->flattenToIndex(bm);

    if (this->needOpBytes(sizeof(int32_t) * 2 + sizeof(bool))) {
        this->writeOp(kDrawSprite_DrawOp, 0, bitmapIndex);
        fWriter.writeBool(paint != NULL);
        fWriter.write32(left);
        fWriter.write32(top);
    }
}

void SkGPipeCanvas::drawText(const void* text, size_t byteLength, SkScalar x,
                                 SkScalar y, const SkPaint& paint) {
    if (byteLength) {
        NOTIFY_SETUP(this);
        this->writePaint(paint);
        if (this->needOpBytes(4 + SkAlign4(byteLength) + 2 * sizeof(SkScalar))) {
            this->writeOp(kDrawText_DrawOp);
            fWriter.write32(byteLength);
            fWriter.writePad(text, byteLength);
            fWriter.writeScalar(x);
            fWriter.writeScalar(y);
        }
    }
}

void SkGPipeCanvas::drawPosText(const void* text, size_t byteLength,
                                const SkPoint pos[], const SkPaint& paint) {
    if (byteLength) {
        NOTIFY_SETUP(this);
        this->writePaint(paint);
        int count = paint.textToGlyphs(text, byteLength, NULL);
        if (this->needOpBytes(4 + SkAlign4(byteLength) + 4 + count * sizeof(SkPoint))) {
            this->writeOp(kDrawPosText_DrawOp);
            fWriter.write32(byteLength);
            fWriter.writePad(text, byteLength);
            fWriter.write32(count);
            fWriter.write(pos, count * sizeof(SkPoint));
        }
    }
}

void SkGPipeCanvas::drawPosTextH(const void* text, size_t byteLength,
                                 const SkScalar xpos[], SkScalar constY,
                                 const SkPaint& paint) {
    if (byteLength) {
        NOTIFY_SETUP(this);
        this->writePaint(paint);
        int count = paint.textToGlyphs(text, byteLength, NULL);
        if (this->needOpBytes(4 + SkAlign4(byteLength) + 4 + count * sizeof(SkScalar) + 4)) {
            this->writeOp(kDrawPosTextH_DrawOp);
            fWriter.write32(byteLength);
            fWriter.writePad(text, byteLength);
            fWriter.write32(count);
            fWriter.write(xpos, count * sizeof(SkScalar));
            fWriter.writeScalar(constY);
        }
    }
}

void SkGPipeCanvas::drawTextOnPath(const void* text, size_t byteLength,
                                   const SkPath& path, const SkMatrix* matrix,
                                   const SkPaint& paint) {
    if (byteLength) {
        NOTIFY_SETUP(this);
        unsigned flags = 0;
        size_t size = 4 + SkAlign4(byteLength) + estimateFlattenSize(path);
        if (matrix) {
            flags |= kDrawTextOnPath_HasMatrix_DrawOpFlag;
            size += matrix->flatten(NULL);
        }
        this->writePaint(paint);
        if (this->needOpBytes(size)) {
            this->writeOp(kDrawTextOnPath_DrawOp, flags, 0);

            fWriter.write32(byteLength);
            fWriter.writePad(text, byteLength);

            path.flatten(fWriter);
            if (matrix) {
                fWriter.writeMatrix(*matrix);
            }
        }
    }
}

void SkGPipeCanvas::drawPicture(SkPicture& picture) {
    // we want to playback the picture into individual draw calls
    this->INHERITED::drawPicture(picture);
}

void SkGPipeCanvas::drawVertices(VertexMode mode, int vertexCount,
                                 const SkPoint vertices[], const SkPoint texs[],
                                 const SkColor colors[], SkXfermode*,
                                 const uint16_t indices[], int indexCount,
                                 const SkPaint& paint) {
    if (0 == vertexCount) {
        return;
    }

    NOTIFY_SETUP(this);
    size_t size = 4 + vertexCount * sizeof(SkPoint);
    this->writePaint(paint);
    unsigned flags = 0;
    if (texs) {
        flags |= kDrawVertices_HasTexs_DrawOpFlag;
        size += vertexCount * sizeof(SkPoint);
    }
    if (colors) {
        flags |= kDrawVertices_HasColors_DrawOpFlag;
        size += vertexCount * sizeof(SkColor);
    }
    if (indices && indexCount > 0) {
        flags |= kDrawVertices_HasIndices_DrawOpFlag;
        size += 4 + SkAlign4(indexCount * sizeof(uint16_t));
    }

    if (this->needOpBytes(size)) {
        this->writeOp(kDrawVertices_DrawOp, flags, 0);
        fWriter.write32(mode);
        fWriter.write32(vertexCount);
        fWriter.write(vertices, vertexCount * sizeof(SkPoint));
        if (texs) {
            fWriter.write(texs, vertexCount * sizeof(SkPoint));
        }
        if (colors) {
            fWriter.write(colors, vertexCount * sizeof(SkColor));
        }

        // TODO: flatten xfermode

        if (indices && indexCount > 0) {
            fWriter.write32(indexCount);
            fWriter.writePad(indices, indexCount * sizeof(uint16_t));
        }
    }
}

void SkGPipeCanvas::drawData(const void* ptr, size_t size) {
    if (size && ptr) {
        NOTIFY_SETUP(this);
        unsigned data = 0;
        if (size < (1 << DRAWOPS_DATA_BITS)) {
            data = (unsigned)size;
        }
        if (this->needOpBytes(4 + SkAlign4(size))) {
            this->writeOp(kDrawData_DrawOp, 0, data);
            if (0 == data) {
                fWriter.write32(size);
            }
            fWriter.writePad(ptr, size);
        }
    }
}

///////////////////////////////////////////////////////////////////////////////

template <typename T> uint32_t castToU32(T value) {
    union {
        T           fSrc;
        uint32_t    fDst;
    } data;
    data.fSrc = value;
    return data.fDst;
}

void SkGPipeCanvas::writePaint(const SkPaint& paint) {
    SkPaint& base = fPaint;
    uint32_t storage[32];
    uint32_t* ptr = storage;

    if (base.getFlags() != paint.getFlags()) {
        *ptr++ = PaintOp_packOpData(kFlags_PaintOp, paint.getFlags());
        base.setFlags(paint.getFlags());
    }
    if (base.getColor() != paint.getColor()) {
        *ptr++ = PaintOp_packOp(kColor_PaintOp);
        *ptr++ = paint.getColor();
        base.setColor(paint.getColor());
    }
    if (base.getStyle() != paint.getStyle()) {
        *ptr++ = PaintOp_packOpData(kStyle_PaintOp, paint.getStyle());
        base.setStyle(paint.getStyle());
    }
    if (base.getStrokeJoin() != paint.getStrokeJoin()) {
        *ptr++ = PaintOp_packOpData(kJoin_PaintOp, paint.getStrokeJoin());
        base.setStrokeJoin(paint.getStrokeJoin());
    }
    if (base.getStrokeCap() != paint.getStrokeCap()) {
        *ptr++ = PaintOp_packOpData(kCap_PaintOp, paint.getStrokeCap());
        base.setStrokeCap(paint.getStrokeCap());
    }
    if (base.getStrokeWidth() != paint.getStrokeWidth()) {
        *ptr++ = PaintOp_packOp(kWidth_PaintOp);
        *ptr++ = castToU32(paint.getStrokeWidth());
        base.setStrokeWidth(paint.getStrokeWidth());
    }
    if (base.getStrokeMiter() != paint.getStrokeMiter()) {
        *ptr++ = PaintOp_packOp(kMiter_PaintOp);
        *ptr++ = castToU32(paint.getStrokeMiter());
        base.setStrokeMiter(paint.getStrokeMiter());
    }
    if (base.getTextEncoding() != paint.getTextEncoding()) {
        *ptr++ = PaintOp_packOpData(kEncoding_PaintOp, paint.getTextEncoding());
        base.setTextEncoding(paint.getTextEncoding());
    }
    if (base.getHinting() != paint.getHinting()) {
        *ptr++ = PaintOp_packOpData(kHinting_PaintOp, paint.getHinting());
        base.setHinting(paint.getHinting());
    }
    if (base.getTextAlign() != paint.getTextAlign()) {
        *ptr++ = PaintOp_packOpData(kAlign_PaintOp, paint.getTextAlign());
        base.setTextAlign(paint.getTextAlign());
    }
    if (base.getTextSize() != paint.getTextSize()) {
        *ptr++ = PaintOp_packOp(kTextSize_PaintOp);
        *ptr++ = castToU32(paint.getTextSize());
        base.setTextSize(paint.getTextSize());
    }
    if (base.getTextScaleX() != paint.getTextScaleX()) {
        *ptr++ = PaintOp_packOp(kTextScaleX_PaintOp);
        *ptr++ = castToU32(paint.getTextScaleX());
        base.setTextScaleX(paint.getTextScaleX());
    }
    if (base.getTextSkewX() != paint.getTextSkewX()) {
        *ptr++ = PaintOp_packOp(kTextSkewX_PaintOp);
        *ptr++ = castToU32(paint.getTextSkewX());
        base.setTextSkewX(paint.getTextSkewX());
    }

    if (!SkTypeface::Equal(base.getTypeface(), paint.getTypeface())) {
        uint32_t id = this->getTypefaceID(paint.getTypeface());
        *ptr++ = PaintOp_packOpData(kTypeface_PaintOp, id);
        base.setTypeface(paint.getTypeface());
    }

    for (int i = 0; i < kCount_PaintFlats; i++) {
        int index = this->flattenToIndex(get_paintflat(paint, i), (PaintFlats)i);
        SkASSERT(index >= 0 && index <= fFlatArray.count());
        if (index != fCurrFlatIndex[i]) {
            *ptr++ = PaintOp_packOpFlagData(kFlatIndex_PaintOp, i, index);
            fCurrFlatIndex[i] = index;
        }
    }

    size_t size = (char*)ptr - (char*)storage;
    if (size && this->needOpBytes(size)) {
        this->writeOp(kPaintOp_DrawOp, 0, size);
        fWriter.write(storage, size);
        for (size_t i = 0; i < size/4; i++) {
//            SkDebugf("[%d] %08X\n", i, storage[i]);
        }
    }
}

///////////////////////////////////////////////////////////////////////////////

#include "SkGPipe.h"

SkGPipeWriter::SkGPipeWriter() : fWriter(0) {
    fCanvas = NULL;
}

SkGPipeWriter::~SkGPipeWriter() {
    this->endRecording();
    SkSafeUnref(fCanvas);
}

SkCanvas* SkGPipeWriter::startRecording(SkGPipeController* controller,
                                        uint32_t flags) {
    if (NULL == fCanvas) {
        fWriter.reset(NULL, 0);
        fFactorySet.reset();
        fCanvas = SkNEW_ARGS(SkGPipeCanvas, (controller, &fWriter,
                                             (flags & kCrossProcess_Flag) ?
                                             &fFactorySet : NULL));
    }
    return fCanvas;
}

void SkGPipeWriter::endRecording() {
    if (fCanvas) {
        fCanvas->finish();
        fCanvas->unref();
        fCanvas = NULL;
    }
}


text:
//
//  DLChartsHeader.h
//  Pods
//
//  Created by sml2 on 2017/6/23.
//
//

#ifndef DLChartsHeader_h
#define DLChartsHeader_h

#import "DLChartConfigureItem.h"

#import "DLChartBaseView.h"
#import "DLRadarChartView.h"
#import "DLVerticalBarChartView.h"
#import "DLPieChartView.h"
#import "DLLineChartView.h"
#import "DLHorizontalBarChartView.h"
#import <Charts/Charts-Swift.h>

#endif /* DLChartsHeader_h */


text:
function Add-VSSageMakerEndpointConfigProductionVariant {
    <#
    .SYNOPSIS
        Adds an AWS::SageMaker::EndpointConfig.ProductionVariant resource property to the template. Specifies a model that you want to host and the resources to deploy for hosting it. If you are deploying multiple models, tell Amazon SageMaker how to distribute traffic among the models by specifying the InitialVariantWeight objects.

    .DESCRIPTION
        Adds an AWS::SageMaker::EndpointConfig.ProductionVariant resource property to the template.
Specifies a model that you want to host and the resources to deploy for hosting it. If you are deploying multiple models, tell Amazon SageMaker how to distribute traffic among the models by specifying the InitialVariantWeight objects.

    .LINK
        http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-productionvariant.html

    .PARAMETER ModelName
        The name of the model that you want to host. This is the name that you specified when creating the model.

        Documentation: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-productionvariant.html#cfn-sagemaker-endpointconfig-productionvariant-modelname
        PrimitiveType: String
        UpdateType: Immutable

    .PARAMETER VariantName
        The name of the production variant.

        Documentation: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-productionvariant.html#cfn-sagemaker-endpointconfig-productionvariant-variantname
        PrimitiveType: String
        UpdateType: Immutable

    .PARAMETER InitialInstanceCount
        Number of instances to launch initially.

        Documentation: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-productionvariant.html#cfn-sagemaker-endpointconfig-productionvariant-initialinstancecount
        PrimitiveType: Integer
        UpdateType: Immutable

    .PARAMETER InstanceType
        The ML compute instance type.

        Documentation: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-productionvariant.html#cfn-sagemaker-endpointconfig-productionvariant-instancetype
        PrimitiveType: String
        UpdateType: Immutable

    .PARAMETER AcceleratorType
        The size of the Elastic Inference EI instance to use for the production variant. EI instances provide on-demand GPU computing for inference. For more information, see Using Elastic Inference in Amazon SageMaker: https://docs.aws.amazon.com/sagemaker/latest/dg/ei.html. For more information, see Using Elastic Inference in Amazon SageMaker: https://docs.aws.amazon.com/sagemaker/latest/dg/ei.html.

        Documentation: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-productionvariant.html#cfn-sagemaker-endpointconfig-productionvariant-acceleratortype
        PrimitiveType: String
        UpdateType: Immutable

    .PARAMETER InitialVariantWeight
        Determines initial traffic distribution among all of the models that you specify in the endpoint configuration. The traffic to a production variant is determined by the ratio of the VariantWeight to the sum of all VariantWeight values across all ProductionVariants. If unspecified, it defaults to 1.0.

        Documentation: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-sagemaker-endpointconfig-productionvariant.html#cfn-sagemaker-endpointconfig-productionvariant-initialvariantweight
        PrimitiveType: Double
        UpdateType: Immutable

    .FUNCTIONALITY
        Vaporshell
    #>
    [OutputType('Vaporshell.Resource.SageMaker.EndpointConfig.ProductionVariant')]
    [cmdletbinding()]
    Param
    (
        [parameter(Mandatory = $true)]
        [ValidateScript( {
                $allowedTypes = "System.String","Vaporshell.Function","Vaporshell.Condition"
                if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    $PSCmdlet.ThrowTerminatingError((New-VSError -String "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."))
                }
            })]
        $ModelName,
        [parameter(Mandatory = $true)]
        [ValidateScript( {
                $allowedTypes = "System.String","Vaporshell.Function","Vaporshell.Condition"
                if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    $PSCmdlet.ThrowTerminatingError((New-VSError -String "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."))
                }
            })]
        $VariantName,
        [parameter(Mandatory = $true)]
        [ValidateScript( {
                $allowedTypes = "System.Int32","Vaporshell.Function"
                if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    $PSCmdlet.ThrowTerminatingError((New-VSError -String "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."))
                }
            })]
        $InitialInstanceCount,
        [parameter(Mandatory = $true)]
        [ValidateScript( {
                $allowedTypes = "System.String","Vaporshell.Function","Vaporshell.Condition"
                if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    $PSCmdlet.ThrowTerminatingError((New-VSError -String "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."))
                }
            })]
        $InstanceType,
        [parameter(Mandatory = $false)]
        [ValidateScript( {
                $allowedTypes = "System.String","Vaporshell.Function","Vaporshell.Condition"
                if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    $PSCmdlet.ThrowTerminatingError((New-VSError -String "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."))
                }
            })]
        $AcceleratorType,
        [parameter(Mandatory = $true)]
        [ValidateScript( {
                $allowedTypes = "System.Double","Vaporshell.Function"
                if ([string]$($_.PSTypeNames) -match "($(($allowedTypes|ForEach-Object{[RegEx]::Escape($_)}) -join '|'))") {
                    $true
                }
                else {
                    $PSCmdlet.ThrowTerminatingError((New-VSError -String "This parameter only accepts the following types: $($allowedTypes -join ", "). The current types of the value are: $($_.PSTypeNames -join ", ")."))
                }
            })]
        $InitialVariantWeight
    )
    Begin {
        $obj = [PSCustomObject]@{}
        $commonParams = @('Verbose','Debug','ErrorAction','WarningAction','InformationAction','ErrorVariable','WarningVariable','InformationVariable','OutVariable','OutBuffer','PipelineVariable')
    }
    Process {
        foreach ($key in $PSBoundParameters.Keys | Where-Object {$commonParams -notcontains $_}) {
            switch ($key) {
                Default {
                    $obj | Add-Member -MemberType NoteProperty -Name $key -Value $PSBoundParameters.$key
                }
            }
        }
    }
    End {
        $obj | Add-ObjectDetail -TypeName 'Vaporshell.Resource.SageMaker.EndpointConfig.ProductionVariant'
        Write-Verbose "Resulting JSON from $($MyInvocation.MyCommand): `n`n$($obj | ConvertTo-Json -Depth 5)`n"
    }
}


text:
<?php

namespace ICanBoogie\Mailer;

use ICanBoogie\Mailer\AddressListTest\AddressListExtended;

class AddressListTest extends \PHPUnit_Framework_TestCase
{
	/**
	 * @dataProvider provide_test_encode_display_name
	 */
	public function test_encode_display_name($display_name, $expected)
	{
		$this->assertEquals($expected, AddressList::escape_display_name($display_name));
	}

	public function provide_test_encode_display_name()
	{
		return [

			[ 'Joe Q. Public', '"Joe Q. Public"' ],
			[ 'Mary Smith', 'Mary Smith' ],
			[ 'Who?', 'Who?' ],
			[ 'Giant; "Big" Box', '"Giant; \"Big\" Box"' ],
			[ 'ACME, Inc.', '"ACME, Inc."' ]

		];
	}

	public function test_get_iterator()
	{
		$r = new AddressList;
		$this->assertInstanceOf('ArrayIterator', $r->getIterator());
	}

	public function test_add_email()
	{
		$r = new AddressList;
		$r[] = 'olivier.laviale@gmail.com';

		$this->assertNotEmpty($r['olivier.laviale@gmail.com']);
	}

	public function test_set_email()
	{
		$r = new AddressList;
		$r['olivier.laviale@gmail.com'] = true;

		$this->assertNotEmpty($r['olivier.laviale@gmail.com']);
	}

	public function test_set_email_with_name()
	{
		$r = new AddressList;
		$r['olivier.laviale@gmail.com'] = 'Olivier Laviale';

		$this->assertNotEmpty($r['olivier.laviale@gmail.com']);
		$this->assertEquals('Olivier Laviale', $r['olivier.laviale@gmail.com']);
	}

	public function test_unset()
	{
		$r = new AddressList;
		$r[] = 'olivier.laviale@gmail.com';

		unset($r['olivier.laviale@gmail.com']);

		$this->assertFalse(isset($r['olivier.laviale@gmail.com']));
	}

	public function test_from_array()
	{
		$r = AddressList::from([

			'person1@example.com',
			'person2@example.org',
			'person3@example.net' => 'Person 3 Name',
			'person4@example.org',
			'person5@example.net' => 'Person 5 Name'

		]);

		$this->assertTrue($r['person1@example.com']);
		$this->assertTrue($r['person2@example.org']);
		$this->assertEquals('Person 3 Name', $r['person3@example.net']);
		$this->assertTrue($r['person4@example.org']);
		$this->assertEquals('Person 5 Name', $r['person5@example.net']);
	}

	public function test_from_string()
	{
		$r = AddressList::from(<<<EOT
person1@example.com  ,
person2@example.org ,    Person 3 Name <person3@example.net>, person4@example.org,
Pérsôn 5 Name <person5@example.net>
EOT
		);

		$this->assertTrue($r['person1@example.com']);
		$this->assertTrue($r['person2@example.org']);
		$this->assertEquals('Person 3 Name', $r['person3@example.net']);
		$this->assertTrue($r['person4@example.org']);
		$this->assertEquals('Pérsôn 5 Name', $r['person5@example.net']);
	}

	/*
	 * Make sure that AddressList::from() returns a `ICanBoogie\Mailer\AddressList` even when the
	 * source is a subclass of `AddressList`.
	 */
	public function test_from_extended()
	{
		$e = AddressListExtended::from('person@example.com');

		$this->assertInstanceOf('ICanBoogie\Mailer\AddressListTest\AddressListExtended', $e);
		$this->assertInstanceOf('ICanBoogie\Mailer\AddressList', AddressList::from($e));
	}

	public function test_to_string()
	{
		$r = new AddressList([

			'person1@example.com',
			'person2@example.org',
			'person3@example.net' => 'Person 3 Name',
			'person4@example.org',
			'person5@example.net' => 'Pérsôn 5 Nàmè'

		]);

		$this->assertEquals('person1@example.com, person2@example.org, Person 3 Name <person3@example.net>, person4@example.org, Pérsôn 5 Nàmè <person5@example.net>', (string) $r);
	}
}

namespace ICanBoogie\Mailer\AddressListTest;

class AddressListExtended extends \ICanBoogie\Mailer\AddressList
{

}