public class EvernoteMars {

    public static final EvernoteMars EVERNOTE = new EvernoteMars();

    private final WifiStateReceiver mWifiStateReceiver = new WifiStateReceiver();

    private static final int TIMEOUT = 15000;

    private static final String BEGIN_NOTE_LOADING = "beginNoteLoading";
    public static final String END_NOTE_LOADING = "endNoteLoading";
    public static final String NUM_NOTES_RETURNED = "numNotesReturned";

    private static final String OPPY_NOTEBOOK_ID = "a7271bf8-0b06-495a-bb48-7c0c7af29f70";
    private static final String MSL_NOTEBOOK_ID = "0296f732-694d-4ccd-9f5b-5983dc98b9e0";
    private static final String SPIRIT_NOTEBOOK_ID = "f1a72415-56e7-4244-8e12-def9be9c512b";

    public static final int NOTE_PAGE_SIZE = 15;

    private NoteStore.Client noteStore;
    private UserStore.Client userStore;
    private PublicUserInfo userInfo;
    private String uriPrefix;

    private static final List<Note> notesArray = Lists.newArrayList();

    private static final Map<String, String> notebookIDs = Maps.newHashMap();

    private static String searchWords = null;

    static {
        notebookIDs.put(Rover.CURIOSITY, MSL_NOTEBOOK_ID);
        notebookIDs.put(Rover.OPPORTUNITY, OPPY_NOTEBOOK_ID);
        notebookIDs.put(Rover.SPIRIT, SPIRIT_NOTEBOOK_ID);
    }

    public boolean hasNotesRemaining = true;

    private EvernoteMars() {
        IntentFilter mIntentFilter = new IntentFilter(MarsImagesApp.MISSION_CHANGED);
        BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction().equals(MarsImagesApp.MISSION_CHANGED)) {
                    searchWords = null;
                    EvernoteMars.reloadNotes(context, true); //reset connection for new mission notebook
                }
            }
        };
        LocalBroadcastManager.getInstance(MARS_IMAGES.getApplicationContext()).registerReceiver(mMessageReceiver,
                mIntentFilter);
    }

    public void loadMoreImages(Context context) {
        loadMoreImages(context, false);
    }

    public void loadMoreImages(Context context, boolean clearFirst) {
        ConnectivityManager cm = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();
        if (!isConnected) {
            IntentFilter intentFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION);
            intentFilter.setPriority(100);
            LocalBroadcastManager.getInstance(MARS_IMAGES.getApplicationContext()).registerReceiver(mWifiStateReceiver, intentFilter);
            Toast.makeText(context, "Unable to connect to the network.", Toast.LENGTH_SHORT).show();
            return;
        }
        EVERNOTE.loadMoreNotes(context, clearFirst);
    }

    public int getNotesCount() {
        return notesArray.size();
    }

    public Note getNote(int noteIndex) {
        if (noteIndex >= 0 && noteIndex < getNotesCount())
            return notesArray.get(noteIndex);
        else
            return null;
    }

    public List<Note> getNotes() { return notesArray; }

    public void loadMoreNotes(Context context, boolean clearNotes) {
        new NoteLoaderTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, context, getNotesCount(), clearNotes);
    }

    public int[] getNearestRMC() {
        int user_site = 0;
        int user_drive = 0;

        //find the RMC of the newest image/note
        for (Note note : notesArray) {
            String title = note.getTitle();
            if (!title.contains("RMC")) {
                continue;
            }
            String rmcString = title.substring(title.length()-13);
            String siteString = rmcString.substring(0,6);
            String driveString = rmcString.substring(7,13);
            user_site = Integer.parseInt(siteString);
            user_drive = Integer.parseInt(driveString);
            break;
        }

        //find the RMC in locations closest to the RMC of the newest image
        List<int[]> locations = MARS_IMAGES.getLocations(MARS_IMAGES.getApplicationContext());
        int[] lastLocation = locations.get(locations.size()-1);
        int siteIndex = lastLocation[0];
        int driveIndex = lastLocation[1];

        if (user_site != 0 || user_drive != 0) {
            for (int i = locations.size()-1; i >=0; i--) {
                int[] location = locations.get(i);
                int aSiteIndex = location[0];
                int aDriveIndex = location[1];
                if (aSiteIndex*100000+aDriveIndex < user_site*100000+user_drive)
                    break;
                siteIndex = aSiteIndex;
                driveIndex = aDriveIndex;
            }
        }

        return new int[] { siteIndex, driveIndex };
    }


    private void connect() throws TException, EDAMSystemException, EDAMUserException, EDAMNotFoundException {
        if (noteStore == null) {
            Stopwatch watch = Stopwatch.createStarted();
            String userStoreUrl = "https://www.evernote.com/edam/user";
            THttpClient userStoreHttpClient = new THttpClient(userStoreUrl);
            TBinaryProtocol userStoreProtocol = new TBinaryProtocol(userStoreHttpClient);
            userStore = new UserStore.Client(userStoreProtocol, userStoreProtocol);
            String user = MARS_IMAGES.getMission().getUser();
            userInfo = userStore.getPublicUserInfo(user);
            uriPrefix = userInfo.getWebApiUrlPrefix();
            String noteStoreUrl = userInfo.getNoteStoreUrl();
            THttpClient noteStoreHttpClient = new THttpClient(noteStoreUrl);
            noteStoreHttpClient.setConnectTimeout(TIMEOUT);
            TBinaryProtocol noteStoreProtocol = new TBinaryProtocol(noteStoreHttpClient);
            noteStore = new NoteStore.Client(noteStoreProtocol, noteStoreProtocol);
            Log.d("evernote connect", "Time to connect to Evernote: " + watch.elapsed(TimeUnit.MILLISECONDS) + " ms");
        }
    }

    public String getNoteUrl(int position) {
        String imageUrl = null;
        if (position >= 0 && position < notesArray.size()) {
            Note note = notesArray.get(position);
            imageUrl = getUri(note.getResources().get(0));
        }
        return imageUrl;
    }

    public String getUri(Resource resource) {
        return EVERNOTE.getUriPrefix() + "res/" + resource.getGuid();
    }

    String getUriPrefix() {
        return uriPrefix;
    }

    public String getThumbnailURL(int position) {
        String thumbnailUrl = null;
        if (position >= 0 && position < notesArray.size()) {
            Note note = notesArray.get(position);
            if (note.getResources().isEmpty())
                return null;
            String guid = note.getResources().get(0).getGuid();
            thumbnailUrl = getUriPrefix() + "thm/res/" + guid + "?size=" + gov.nasa.jpl.hi.marsimages.ui.ImageListFragment.THUMBNAIL_IMAGE_WIDTH;
        }
        return thumbnailUrl;
    }

    public class NoteLoaderTask extends AsyncTask<Object, Void, String> {

        private Context context;

        @Override
        protected String doInBackground(Object... params) {
            if (!(params[0] instanceof Context) ||
                    !(params[1] instanceof Integer) ||
                    !(params[2] instanceof Boolean)) {
                Log.e(getClass().toString(), "Unexpected input parameters");
            }
            context = (Context) params[0];
            Integer startIndex = (Integer) params[1];
            final Boolean clearNotes = (Boolean) params[2];

            if (clearNotes) {
                notesArray.clear();
                startIndex = 0;
                hasNotesRemaining = true;
                Intent intent = new Intent(MarsImagesApp.NOTES_CLEARED);
                LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
            }

            if (!hasNotesRemaining) {
                Intent intent = new Intent(END_NOTE_LOADING);
                intent.putExtra(NUM_NOTES_RETURNED, 0);
                LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
                return null;
            }

            ConnectivityManager cm =
                    (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
            boolean isConnected = activeNetwork != null &&
                    activeNetwork.isConnectedOrConnecting();
            if (!isConnected) {
                hasNotesRemaining = false;
                Intent intent = new Intent(END_NOTE_LOADING);
                intent.putExtra(NUM_NOTES_RETURNED, 0);
                LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
                return "Unable to connect to the network.";
            }

            int notesReturned = 0;

            try {
                if (noteStore == null || userStore == null)
                    connect();
            } catch (Exception e) {
                Log.w("service error", "Error connecting to Evernote: " + e);
                suspendEvernoteQueries();
                Intent intent = new Intent(END_NOTE_LOADING);
                intent.putExtra(NUM_NOTES_RETURNED, 0);
                LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
                return "The Mars image service is currently unavailable. Please try again later.";
            }

            if (notesArray.size() > startIndex) {
                return null;
            }

            try {
                Intent intent = new Intent(BEGIN_NOTE_LOADING);
                LocalBroadcastManager.getInstance(context).sendBroadcast(intent);

                NoteFilter filter = new NoteFilter();
                filter.setNotebookGuid(notebookIDs.get(MARS_IMAGES.getMissionName()));
                filter.setOrder(NoteSortOrder.TITLE.getValue());
                filter.setAscending(false);
                if (searchWords != null && !searchWords.isEmpty()) {
                    filter.setWords(formatSearch(searchWords));
                }
                NoteList notelist = noteStore.findNotes(null, filter, startIndex, NOTE_PAGE_SIZE);
                notesReturned = notelist.getNotes().size();
                hasNotesRemaining = notelist.getTotalNotes() - (startIndex + notesReturned) > 0;
                for (Note note : notelist.getNotes()) {
                    Note orderedNote = reorderResources(note);
                    notesArray.add(orderedNote);
                }
            } catch (Exception e) {
                Log.w("service error", "Error querying Evernote: " + e);
                suspendEvernoteQueries();
                Intent intent = new Intent(END_NOTE_LOADING);
                intent.putExtra(NUM_NOTES_RETURNED, notesReturned);
                LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
                return "The Mars image service is currently unavailable. Please try again later.";
            }

            Intent intent = new Intent(END_NOTE_LOADING);
            intent.putExtra(NUM_NOTES_RETURNED, notesReturned);
            LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
            return null;
        }

        @Override
        protected void onPostExecute(String message) {
            if (message != null && context instanceof Activity) {
                Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void suspendEvernoteQueries() {
        hasNotesRemaining = false;
        new Thread() {
            @Override
            public void run() {
                try {
                    sleep(30 * 1000);
                } catch (InterruptedException e) {
                    //no need to handle
                } finally {
                    hasNotesRemaining = true;
                }
            }
        }.start();
    }

    private Note reorderResources(Note note) {
        List<String> resourceFilenames = Lists.newArrayList();
        Map<String, Resource> resourcesByFile = Maps.newHashMap();
        for (Resource resource : note.getResources()) {
            Rover mission = MARS_IMAGES.getMission();
            String filename = mission.getSortableImageFilename(resource.getAttributes().getSourceURL());
            resourceFilenames.add(filename);
            resourcesByFile.put(filename, resource);
        }
        Collections.sort(resourceFilenames);
        List<Resource> sortedResources = Lists.newArrayList();
        for (String resourceFilename : resourceFilenames) {
            sortedResources.add(resourcesByFile.get(resourceFilename));
        }
        note.setResources(sortedResources);
        return note;
    }

    public static void reloadNotes(Context context) {
        reloadNotes(context, false);
    }

    public static void reloadNotes(Context context, boolean resetConnection) {
        EVERNOTE.hasNotesRemaining = true;
        if (resetConnection) {
            EVERNOTE.noteStore = null;
            EVERNOTE.userStore = null;
            EVERNOTE.userInfo = null;
            EVERNOTE.uriPrefix = null;
        }
        EVERNOTE.loadMoreNotes(context, true);
    }

    public static void setSearchWords(String searchWords, Context context) {
        EvernoteMars.searchWords = searchWords;
        EvernoteMars.reloadNotes(context);
    }

    private String formatSearch(String text) {
        String[] words = text.split("\\s+");
        StringBuilder formattedText = new StringBuilder();
        for (String w : words) {
            String word = w;
            int value = 0;
            try {
                value = Integer.parseInt(word);
            } catch (NumberFormatException e) {
                Log.w("search words", "Expected an integer value instead of "+word);
            }
            if (value > 0 && !word.endsWith("*")) {
                word = String.format("\"Sol %05d\"", value);
            }

            if (formattedText.length() > 0) {
                formattedText.append(" ");
            }

            formattedText.append(String.format("intitle:%s", word));
        }
        Log.d("search words", "formatted text: " + formattedText);
        return formattedText.toString();
    }

    public static class WifiStateReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            ConnectivityManager cm =
                    (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
            boolean isConnected = activeNetwork != null && activeNetwork.isConnectedOrConnecting();
            if (isConnected) {
                Log.d("wifi state connected", "Wifi reconnected.");
                LocalBroadcastManager.getInstance(context).unregisterReceiver(this);
                EVERNOTE.hasNotesRemaining = true;
                EVERNOTE.loadMoreNotes(context, false);
            }
        }
    }
} public abstract class CameraModel implements Model {

    public static final String TAG = "CameraModel";

    private double[] d1 = new double[3], d2 = new double[3];

    public abstract int[] size();

    public static Model getModel(JSONArray modelJSON) {
        Model returnedModel = null;
        JSONArray c, a, h, v, o, r, e;
        int mtype;
        double mparm;
        int width = 0, height = 0;
        String type = null;
        try {
            JSONObject imageDimensions = modelJSON.getJSONObject(0);
            JSONObject geometricModel = modelJSON.getJSONObject(1);
            type = geometricModel.getString("type");
            JSONObject comps = geometricModel.getJSONObject("components");
            JSONArray area = imageDimensions.getJSONArray("area");
            c = comps.getJSONArray("c");
            a = comps.getJSONArray("a");
            h = comps.getJSONArray("h");
            v = comps.getJSONArray("v");
            width = area.getInt(0);
            height = area.getInt(1);

            if ("CAHV".equals(type)) {
                CAHV model = new CAHV();
                model.setC(c.getDouble(0), c.getDouble(1), c.getDouble(2));
                model.setA(a.getDouble(0), a.getDouble(1), a.getDouble(2));
                model.setH(h.getDouble(0), h.getDouble(1), h.getDouble(2));
                model.setV(v.getDouble(0), v.getDouble(1), v.getDouble(2));
                returnedModel = model;
            } else if ("CAHVOR".equals(type)) {
                o = comps.getJSONArray("o");
                r = comps.getJSONArray("r");
                CAHVOR model = new CAHVOR();
                model.setC(c.getDouble(0), c.getDouble(1), c.getDouble(2));
                model.setA(a.getDouble(0), a.getDouble(1), a.getDouble(2));
                model.setH(h.getDouble(0), h.getDouble(1), h.getDouble(2));
                model.setV(v.getDouble(0), v.getDouble(1), v.getDouble(2));
                model.setO(o.getDouble(0), o.getDouble(1), o.getDouble(2));
                model.setR(r.getDouble(0), r.getDouble(1), r.getDouble(2));
                returnedModel = model;
            } else if ("CAHVORE".equals(type)) {
                o = comps.getJSONArray("o");
                r = comps.getJSONArray("r");
                e = comps.getJSONArray("e");
                mtype = comps.getInt("t");
                mparm = comps.getDouble("p");
                CAHVORE model = new CAHVORE();
                model.setC(c.getDouble(0), c.getDouble(1), c.getDouble(2));
                model.setA(a.getDouble(0), a.getDouble(1), a.getDouble(2));
                model.setH(h.getDouble(0), h.getDouble(1), h.getDouble(2));
                model.setV(v.getDouble(0), v.getDouble(1), v.getDouble(2));
                model.setO(o.getDouble(0), o.getDouble(1), o.getDouble(2));
                model.setR(r.getDouble(0), r.getDouble(1), r.getDouble(2));
                model.setE(e.getDouble(0), e.getDouble(1), e.getDouble(2));
                model.setType(mtype);
                model.setP(mparm);
                returnedModel = model;
            }
            else {
                Log.e(TAG, "Unknown camera model type: "+type);
            }
        } catch (JSONException ex) {
            Log.e(TAG, "Error parsing model json ", ex);
        }

        if (returnedModel != null) {
            ((CAHV)returnedModel).setXdim(width);
            ((CAHV)returnedModel).setYdim(height);
        }
        return returnedModel;
    }

    public int[] origin(JSONArray modelJson) {
        try {
            JSONObject imageDimensions = modelJson.getJSONObject(0);
            JSONArray comps = imageDimensions.getJSONArray("origin");
            if (comps != null) {
                int x = comps.getInt(0);
                int y = comps.getInt(1);
                return new int[]{x, y};
            }
            Log.e(TAG, "Brown alert: origin not found in camera model.");
        } catch (JSONException ex) {
            Log.e(TAG, "Error parsing model json", ex);
        }
        return new int[] {0,0};
    }

    public static double[] pointingVector(JSONArray modelJson) {
        try {
            JSONObject cmod = modelJson.getJSONObject(1);
            JSONArray comps = cmod.getJSONArray("camera_vector");
            if (comps != null) {
                double x = comps.getDouble(0);
                double y = comps.getDouble(1);
                double z = comps.getDouble(2);
                return new double[]{x, y, z};
            }
            Log.e(TAG, "Brown alert: camera_vector not found in camera model.");
        } catch (JSONException ex) {
            Log.e(TAG, "error parsing model json", ex);
        }
        return new double[] {};
    }

    public static double angularDistance(double[] v1, double[] v2) {
        if (v1.length==0 || v2.length==0)
            return 0;

        double dot = M.dot(v1, v2);
        return Math.acos(dot);
    }


} public class ImageQuad extends Quad {

    static final double x_axis[] = {1,0,0};
    static final double z_axis[] = {0,0,1};
    static final int numberOfPositions = 4;
    private static final String TAG = "ImageQuad";
    private final Number3D sphereVector;
    private final double roverCameraFOVRadians;
    private String cameraId = null;
    private final int layer;

    private double boundingSphereRadius;
    private String imageId;
    private final float[] center = new float[3];
    private final Number3D mBoundsCenter;

    public boolean isLoading() {
        return loading;
    }

    private boolean loading = false;
    private boolean cancelLoadingRequest = false;
    private int originallyRequestedResolution = 0;
    private Model model;

    static final float textureCoords[] = {0.f, 0.f, 0.f, 1.f, 1.f, 0.f, 1.f, 1.f};
    public TextureInfo textureToAdd = null;

    public ImageQuad(Model model, double[] qLL, String imageID) {
        super();
        this.imageId = imageID;
        this.model = model;
        String cameraId = MARS_IMAGES.getMission().getCameraId(imageID);
        this.cameraId = cameraId;
        roverCameraFOVRadians = MARS_IMAGES.getMission().getCameraFOV(cameraId);
        int layer = 5 + MARS_IMAGES.getMission().getLayer(cameraId, imageID);
        this.layer = layer;

        float[][] vertices = new float[4][];
        for (int i = 0; i < 4; i++) { vertices[i] = new float[3]; }
        getImageVertices(model, qLL, vertices, layer);
        System.arraycopy(vertices[0], 0, v0, 0, 3);
        System.arraycopy(vertices[1],0,v1,0,3);
        System.arraycopy(vertices[2],0,v2,0,3);
        System.arraycopy(vertices[3],0,v3,0,3);

        init();

        center[0] = (v0[0]+v2[0])/2;
        center[1] = (v0[1]+v2[1])/2;
        center[2] = (v0[2]+v2[2])/2;
        mBoundsCenter = new Number3D(center[0], center[1], center[2]);
        sphereVector = new Number3D(mBoundsCenter);
        sphereVector.normalize();
        //assign to the radius the distance from the center to the farthest vertex
        double d0 = distanceBetween(center, v0);
        double d1 = distanceBetween(center, v1);
        double d2 = distanceBetween(center, v2);
        double d3 = distanceBetween(center, v3);

        boundingSphereRadius = d0;
        if (d1>boundingSphereRadius) boundingSphereRadius=d1;
        if (d2>boundingSphereRadius) boundingSphereRadius=d2;
        if (d3>boundingSphereRadius) boundingSphereRadius=d3;
    }

    public Number3D getBoundsCenter() {
        return mBoundsCenter;
    }

    public double getBoundsRadius() {
        return boundingSphereRadius;
    }

    private double cameraFOVRadians() {
        if (cameraId != null)
            return MARS_IMAGES.getMission().getCameraFOV(cameraId);
        throw new IllegalStateException("roverCameraFOVRadians called before cameraId initialized");
    }

    private double distanceBetween(float[] pt1, float[] pt2) {
        float dx = pt1[0]-pt2[0];
        float dy = pt1[1]-pt2[1];
        float dz = pt1[2]-pt2[2];
        return Math.sqrt(dx*dx+dy*dy+dz*dz);
    }

    private void getImageVertices(Model model, double[] qLL, float[][] vertices, float distance) {

        Rover mission = MARS_IMAGES.getMission();
        double eye[] = new double[3];
        double pos[] = new double[2], pos3[] = new double[3], vec3[] = new double[3];
        double pos3LL[]= new double[3], pinitial[] = new double[3], pfinal[] = new double[3];
        double xrotq[] = new double[4];
        M.quatva(x_axis, Math.PI/2, xrotq);
        double zrotq[] = new double[4];
        M.quatva(z_axis, -Math.PI/2, zrotq);
        double llRotq[] = new double[4];

        eye[0] = mission.getMastX();
        eye[1] = mission.getMastY();
        eye[2] = mission.getMastZ();

        llRotq[0] = qLL[0];
        llRotq[1] = qLL[1];
        llRotq[2] = qLL[2];
        llRotq[3] = qLL[3];

        //lower left
        pos[0] = 0;
        pos[1] = model.ydim();
        model.cmod_2d_to_3d(pos, pos3, vec3);
        pos3[0] -= eye[0];
        pos3[1] -= eye[1];
        pos3[2] -= eye[2];
        pos3[0] += vec3[0]*distance;
        pos3[1] += vec3[1]*distance;
        pos3[2] += vec3[2]*distance;
        M.multqv(llRotq, pos3, pos3LL);
        M.multqv(zrotq, pos3LL, pinitial);
        M.multqv(xrotq, pinitial, pfinal);
        vertices[0][0] = (float)pfinal[0];
        vertices[0][1] = (float)pfinal[1];
        vertices[0][2] = (float)pfinal[2];

        //upper left
        pos[0] = 0;
        pos[1] = 0;
        model.cmod_2d_to_3d(pos, pos3, vec3);
        pos3[0] -= eye[0];
        pos3[1] -= eye[1];
        pos3[2] -= eye[2];
        pos3[0] += vec3[0]*distance;
        pos3[1] += vec3[1]*distance;
        pos3[2] += vec3[2]*distance;
        M.multqv(llRotq, pos3, pos3LL);
        M.multqv(zrotq, pos3LL, pinitial);
        M.multqv(xrotq, pinitial, pfinal);
        vertices[1][0] = (float)pfinal[0];
        vertices[1][1] = (float)pfinal[1];
        vertices[1][2] = (float)pfinal[2];

        //lower right
        pos[0] = model.xdim();
        pos[1] = model.ydim();
        model.cmod_2d_to_3d(pos, pos3, vec3);
        pos3[0] -= eye[0];
        pos3[1] -= eye[1];
        pos3[2] -= eye[2];
        pos3[0] += vec3[0]*distance;
        pos3[1] += vec3[1]*distance;
        pos3[2] += vec3[2]*distance;
        M.multqv(llRotq, pos3, pos3LL);
        M.multqv(zrotq, pos3LL, pinitial);
        M.multqv(xrotq, pinitial, pfinal);
        vertices[2][0] = (float)pfinal[0];
        vertices[2][1] = (float)pfinal[1];
        vertices[2][2] = (float)pfinal[2];

        //upper right
        pos[0] = model.xdim();
        pos[1] = 0;
        model.cmod_2d_to_3d(pos, pos3, vec3);
        pos3[0] -= eye[0];
        pos3[1] -= eye[1];
        pos3[2] -= eye[2];
        pos3[0] += vec3[0]*distance;
        pos3[1] += vec3[1]*distance;
        pos3[2] += vec3[2]*distance;
        M.multqv(llRotq, pos3, pos3LL);
        M.multqv(zrotq, pos3LL, pinitial);
        M.multqv(xrotq, pinitial, pfinal);
        vertices[3][0] = (float)pfinal[0];
        vertices[3][1] = (float)pfinal[1];
        vertices[3][2] = (float)pfinal[2];
    }

    public boolean cameraIsLookingAtMe(Camera viewPortCamera) {
        Number3D cameraPointing = viewPortCamera.getLookAt();

        double angleBetweenRadians = Math.acos(Number3D.dot(cameraPointing, sphereVector));

//        return angleBetweenRadians - Math.toRadians(camera.getFieldOfView()) - roverCameraFOVRadians <= 0;
        return angleBetweenRadians - roverCameraFOVRadians < Math.toRadians(viewPortCamera.getFieldOfView())*.7071;
    }


    public void loadImageAndTexture(final Note photo, final String title, final int resolution, final MarsMosaicRenderer renderer) {
        if (photo == null) {
            Log.e(TAG, "No photo in scene with title " + title);
            return;
        }

        synchronized (this) {
            cancelLoadingRequest = false;
            if (loading) {
                return;
            }
            loading = true;
            final int bestTextureResolution = computeBestTextureResolution(resolution);
//            Log.d(TAG, "Best texture resolution: "+bestTextureResolution);
            originallyRequestedResolution = bestTextureResolution;
        }

        ImageLoader.getInstance().resume(); //in case the image loader engine is currently paused
        final String uri = EVERNOTE.getUri(photo.getResources().get(0));
        ImageLoader.getInstance().loadImage(uri, new ImageSize(originallyRequestedResolution, originallyRequestedResolution), new SimpleImageLoadingListener() {
            @Override
            public void onLoadingComplete(String imageUri, View view, final Bitmap loadedImage) {

                //if this image is no longer in the viewport, early out
                synchronized (ImageQuad.this) {
                    if (!loading) {
                        return;
                    }

                    if (cancelLoadingRequest) {
                        loading = false;
                        cancelLoadingRequest = false;
                        return;
                    }

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

                    if (!getTextureInfoList().isEmpty()) {
                        int testWidth = width, testHeight = height;
                        if (testWidth != originallyRequestedResolution || testHeight != originallyRequestedResolution) {
                            testWidth = originallyRequestedResolution; testHeight = originallyRequestedResolution;
                        }
                        TextureInfo info = getTextureInfoList().get(0);
                        if (testWidth == info.getWidth() || testHeight == info.getHeight()) {
                            loading = false;
                            return;
                        }
                    }

//                    Log.d(TAG, "Loaded image size: " + width + "x" + height + " for image " + title);
                    final Bitmap texture = (width != originallyRequestedResolution || height != originallyRequestedResolution) ?
                            Bitmap.createScaledBitmap(loadedImage, originallyRequestedResolution, originallyRequestedResolution, true) : loadedImage;
                    if (!loading) return;

                    int bestTextureResolution = computeBestTextureResolution(resolution);
                    if (bestTextureResolution != originallyRequestedResolution) {
                        Log.d(TAG, "Texture resolution mismatch: "+bestTextureResolution + " vs. "+originallyRequestedResolution);
                        loading = false;
                        return;
                    }

                    renderer.getSurfaceView().queueEvent(new Runnable() {
                        @Override
                        public void run() {

                            Runnable runnableForGL = new Runnable() {
                                @Override
                                public void run() {
                                    renderer.getTextureManager().removeTextures(getTextureInfoList());
                                    for (TextureInfo textureInfo : getTextureInfoList())
                                        removeTexture(textureInfo);
                                    getTextureInfoList().clear();
                                    final TextureInfo textureInfo = renderer.getTextureManager().addTexture(texture, TextureManager.TextureType.DIFFUSE, false, false);

                                    setDrawingMode(GL_TRIANGLE_FAN);
                                    setMaterial(new SimpleMaterial());
                                    addTexture(textureInfo);
                                    loading = false;
                                }
                            };
                            renderer.glRunnables.add(runnableForGL);
                        }
                    });
                }
            }

            @Override
            public void onLoadingCancelled(String imageUri, View view) {
                Log.e(TAG, "Loading cancelled for "+imageUri);
                loading = false;
                super.onLoadingCancelled(imageUri, view);
            }

            @Override
            public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
                Log.e(TAG, "Loading failed for "+imageUri);
                loading = false;
                super.onLoadingFailed(imageUri, view, failReason);
            }
        });
    }

    private int computeBestTextureResolution(int resolution) {
        int largestImageDimension = (int)Math.max(model.xdim(), model.ydim());
        int bestImageResolution = Math.min(largestImageDimension, resolution);
//        Log.d(TAG, "Texture res: " + M.floorPowerOfTwo(bestImageResolution));
        return M.floorPowerOfTwo(bestImageResolution);
    }

    public void stopLoading() {
        synchronized (this) {
            cancelLoadingRequest = true;
        }
    }
} public class M {
    public static final double MAT3_EPSILON = 1e-7;
    public static final double LOG2 = Math.log(2);

    public static boolean epsilonEquals(double a, double b) {
        return Math.abs(a - b) <= 0.001;
    }

    public static boolean isPowerOfTwo(int value) {
        return ((value & -value) == value);
    }

    public static int ceilingPowerOfTwo(double x) {
        double y = Math.ceil(Math.log(x)/LOG2);
        return (int)Math.pow(2, y);
    }

    public static int floorPowerOfTwo(double x) {
        double y = Math.floor(Math.log(x)/LOG2);
        return (int)Math.pow(2, y);
    }

    public static int nextHighestPowerOfTwo(int n) {
        double y = Math.floor(Math.log(n)/LOG2);
        return (int)Math.pow(2, y + 1);
    }

    public static int nextLowestPowerOfTwo(int n) {
        double y = Math.floor(Math.log(n)/LOG2);
        return (int)Math.pow(2, y - 1);
    }

    public static void copy(final double[] a, double[] b) {
        b[0] = a[0];
        b[1] = a[1];
        b[2] = a[2];
    }

    public static void scale(final double s, final double[] a, double[] b) {
        b[0] = s*a[0];
        b[1] = s*a[1];
        b[2] = s*a[2];
    }

    public static void add(final double[] a, final double[] b, double[] c) {
        c[0] = a[0]+b[0];
        c[1] = a[1]+b[1];
        c[2] = a[2]+b[2];
    }

    public static void sub(final double[] a, final double[] b, double[] c) {
        c[0] = a[0]-b[0];
        c[1] = a[1]-b[1];
        c[2] = a[2]-b[2];
    }

    public static void cross(final double[] a, final double[] b, double[] c) {
        c[0]  =  a[1] * b[2] - a[2] * b[1];
        c[1]  =  a[2] * b[0] - a[0] * b[2];
        c[2]  =  a[0] * b[1] - a[1] * b[0];
    }

    public static double dot(final double[] a, final double[] b) {
        return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
    }

    public static double mag(final double[] a) {
        return Math.sqrt(a[0] * a[0] + a[1] * a[1] + a[2] * a[2]);
    }

    public static void unit(final double[] a, double[] b) {
        double mag = Math.sqrt(a[0] * a[0]  +  a[1] * a[1]  +  a[2] * a[2]);
        b[0] = a[0] / mag;
        b[1] = a[1] / mag;
        b[2] = a[2] / mag;
    }

    public static void quatva(final double[] v, final double a, double[] q) {
        double c;
        double s;
        double vmag;

    /* Precompute some needed quantities */
        vmag = Math.sqrt(v[0] * v[0]  +  v[1] * v[1]  +  v[2] * v[2]);
        if (vmag < MAT3_EPSILON) {
            return;
        }
        c = Math.cos(a/2);
        s = Math.sin(a/2);

    /* Construct the quaternion */
        q[0] = c;
        q[1] = s * v[0] / vmag;
        q[2] = s * v[1] / vmag;
        q[3] = s * v[2] / vmag;
    }

    public static void multqv(final double[] q, final double[] v, double[] u) {
        double q0;
        double q1;
        double q2;
        double q3;
        double q0q0;
        double q0q1;
        double q0q2;
        double q0q3;
        double q1q1;
        double q1q2;
        double q1q3;
        double q2q2;
        double q2q3;
        double q3q3;

    /* Perform the multiplication */
        q0 = q[0];
        q1 = q[1];
        q2 = q[2];
        q3 = q[3];
        q0q0 = q0 * q0;
        q0q1 = q0 * q1;
        q0q2 = q0 * q2;
        q0q3 = q0 * q3;
        q1q1 = q1 * q1;
        q1q2 = q1 * q2;
        q1q3 = q1 * q3;
        q2q2 = q2 * q2;
        q2q3 = q2 * q3;
        q3q3 = q3 * q3;
        u[0] = v[0]*(q0q0+q1q1-q2q2-q3q3) + 2*v[1]*(q1q2-q0q3) + 2*v[2]*(q0q2+q1q3);
        u[1] = 2*v[0]*(q0q3+q1q2) + v[1]*(q0q0-q1q1+q2q2-q3q3) + 2*v[2]*(q2q3-q0q1);
        u[2] = 2*v[0]*(q1q3-q0q2) + 2*v[1]*(q0q1+q2q3) + v[2]*(q0q0-q1q1-q2q2+q3q3);
    }

/*
 * Convert spherical coordinates to cartesian.
 * The azimuth will range between 0 to 2*PI, measured from the positive x axis,
 * increasing towards the positive y axis.
 * The declination will range between 0 and PI, measured from the positive Z axis
 * (assumed to be down), increasing towards the xy plane.
 */
    public static void sphericalToCartesian(double az, double dec, double radius, double[] xyz) {
        double rsinDec = radius * Math.sin(dec);
        xyz[0] = rsinDec * Math.cos(az);
        xyz[1] = rsinDec * Math.sin(az);
        xyz[2] = radius * Math.cos(dec);
    }

    public static void cartesianToSpherical(final double[] xyz, double[] azDecR) {
        double x = xyz[0], y = xyz[1], z = xyz[2];
        double radius = Math.sqrt(x * x + y * y + z * z);
        double dec = Math.acos(z / radius);
        double az = Math.atan2(y, x);
        if (az < 0) az += 2*Math.PI;
        azDecR[0] = az;
        azDecR[1] = dec;
        azDecR[2] = radius;
    }

} public interface Model {

    double xdim();
    double ydim();

    void cmod_2d_to_3d(double pos2[], double pos3[], double uvec3[]);
    void cmod_3d_to_2d(double pos3[], double range[], double pos2[]);

} public class Quad extends BaseObject3D {


    protected final float[] v0 = new float[3];
    protected final float[] v1 = new float[3];
    protected final float[] v2 = new float[3];
    protected final float[] v3 = new float[3];
    public static final int NUM_VERTICES = 4;

    public Quad() {
        super();
    }

    public Quad(float[] v0, float[] v1, float[] v2, float[] v3) {
        super();
        System.arraycopy(v0, 0, this.v0, 0, 3);
        System.arraycopy(v1, 0, this.v1, 0, 3);
        System.arraycopy(v2, 0, this.v2, 0, 3);
        System.arraycopy(v3,0,this.v3,0,3);

        init();
    }

    public Quad(float[] v0, float[] v1, float[] v2, float[] v3, float[] normal) {
        this(v0, v1, v2, v3);
        float[] normals = new float[NUM_VERTICES*3];
        for (int i = 0; i < NUM_VERTICES; i++)
            System.arraycopy(normal, 0, normals, i*3, 3);
        mGeometry.setNormals(normals);
    }

    protected void init() {
        float[] vertices = new float[NUM_VERTICES * 3];
        float[] textureCoords = new float[NUM_VERTICES * 2];
        float[] normals = new float[NUM_VERTICES * 3];
        float[] colors = new float[NUM_VERTICES * 4];
        int[] indices = new int[4];

        System.arraycopy(v0, 0, vertices, 0, 3);
        System.arraycopy(v1, 0, vertices, 3, 3);
        System.arraycopy(v2, 0, vertices, 6, 3);
        System.arraycopy(v3, 0, vertices, 9, 3);

        textureCoords[0] = 0.0f;
        textureCoords[1] = 1.0f;
        textureCoords[2] = 0.0f;
        textureCoords[3] = 0.0f;
        textureCoords[4] = 1.0f;
        textureCoords[5] = 1.0f;
        textureCoords[6] = 1.0f;
        textureCoords[7] = 0.0f;

        //give every vertex a Z=1 normal
        for (int i = 0; i < NUM_VERTICES; i++) {
            normals[i*3] = 0;
            normals[i*3 + 1] = 0;
            normals[i*3 + 2] = 0;
        }

        int ul = 0;
        int ll = 1;
        int ur = 2;
        int lr = 3;

        indices[0] = ll;
        indices[1] = ul;
        indices[2] = ur;
        indices[3] = lr;

        int numColors = NUM_VERTICES * 4;
        for (int j = 0; j < numColors; j += 4) {
            colors[j] = 1.0f;
            colors[j + 1] = 1.0f;
            colors[j + 2] = 1.0f;
            colors[j + 3] = 1.0f;
        }

        setData(vertices, normals, textureCoords, colors, indices);
    }
} public abstract class Rover {

    public static final String CURIOSITY = "Curiosity";
    public static final String OPPORTUNITY = "Opportunity";
    public static final String SPIRIT = "Spirit";

    public static final String TAG = "Rover";

    static final String SOL = "Sol";
    static final String LTST = "LTST";
    static final String RMC = "RMC";

    private static final double EARTH_SECS_PER_MARS_SEC = 1.027491252;
    private static final DateFormat DATE_FORMAT = SimpleDateFormat.getDateInstance();
    static final String EPOCH_FORMAT = "yyyyMMddhh:mm:sszzz";

    int instrumentIndex;
    int eyeIndex;
    int sampleTypeIndex;

    Map<Integer, List<CSVRecord>> locationsBySite = new HashMap<Integer, List<CSVRecord>>();

    final Set<String> stereoInstruments = new HashSet<>();

    public abstract String getUser();

    public abstract String getSortableImageFilename(String sourceURL);

    public abstract String getLabelText(Note note);

    public String getSectionText(Note note) {
        int sol = getSol(note);
        double interval = sol * 24 * 60 * 60 * EARTH_SECS_PER_MARS_SEC;
        GregorianCalendar calendar = new GregorianCalendar();
        Date epoch = getEpoch();
        calendar.setTime(epoch);
        calendar.add(Calendar.SECOND, (int) interval);
        String formattedDate = DATE_FORMAT.format(calendar.getTime());
        return "Sol " + sol + "  " + formattedDate;
    }

    public abstract int getSol(Note note);

    protected abstract Date getEpoch();

    public abstract String getURLPrefix();

    public abstract String getDetailText(Note note);

    public abstract String getCaptionText(Note note);

    public abstract String getImageName(Resource resource);

    public abstract double getCameraFOV(String cameraId);

    public abstract String getCameraId(String imageID);

    public abstract float getMastX();

    public abstract float getMastY();

    public abstract float getMastZ();

    public String getImageID(Resource resource) {
        String url = resource.getAttributes().getSourceURL();
        String[] tokens = url.split("[\\./]");
        int numTokens = tokens.length;
        return tokens[numTokens - 2];
    }

    public abstract String[] stereoForImages(Note note);

    public JSONArray modelJson(Note note) {
        try {
            String cameraModelJson = note.getResources().get(0).getAttributes().getCameraModel();
            if (cameraModelJson != null && cameraModelJson.length() > 0) {
                try {
                    return new JSONArray(cameraModelJson);
                } catch (JSONException e) {
                    Log.e(TAG, "Bad json in image note " + note.getTitle() + " " + e.getMessage());
                }
            }
        }catch (NullPointerException e) {
            Log.e(TAG, "Null stuff "+ note + ", "+ note.getResources().size());
        }
        return null;
    }

    public double fieldOfView(Note image) {
        String imageID = getImageID(image.getResources().get(0));
        String cameraId = getCameraId(imageID);
        return getCameraFOV(cameraId);
    }

    public static boolean includedInMosaic(Note note) {
        String title = note.getTitle();
        return title.contains("Navcam") ||
                title.contains("Mastcam Left") ||
                title.contains("Pancam");
    }

    public List<CSVRecord> siteLocationData(int siteIndex) {
        //return cached results if we have them
        //TODO invalidate cache when needed (after a day?)
        List<CSVRecord> cachedLocations = locationsBySite.get(siteIndex);
        if (cachedLocations != null) return cachedLocations;

        List<CSVRecord> locations = new ArrayList<>();
        URL locationsURL = null;

        try {
            String sixDigitSite = String.format("%06d", siteIndex);
            locationsURL = new URL(getURLPrefix() + "/locations/site_"+sixDigitSite+".csv");
            Log.d(TAG, "location url: %@" + locationsURL);

            HttpURLConnection urlConnection = (HttpURLConnection)locationsURL.openConnection();
            try {
                InputStream in = new BufferedInputStream(urlConnection.getInputStream());
                final Reader reader = new InputStreamReader(in);
                final CSVParser parser = new CSVParser(reader, CSVFormat.DEFAULT);
                try {
                    for (final CSVRecord record : parser) {
                        locations.add(record);
                    }
                    locationsBySite.put(siteIndex, locations);
                } finally {
                    parser.close();
                    reader.close();
                }
            } finally {
                urlConnection.disconnect();
            }
        } catch (MalformedURLException e) {
            Log.e(TAG, "Badly formatted URL for location manifest: " + locationsURL);
        } catch (IOException e) {
            Log.e(TAG, "Error reading from location manifest URL: " + locationsURL);
        }
        return locations;
    }

    public double[] localLevelQuaternion(int siteIndex, int driveIndex) {
        List<CSVRecord> locationData = siteLocationData(siteIndex);
        double[] q = new double[4];
        for (CSVRecord location : locationData) {
            if (location.size() >= 5 && driveIndex == Integer.parseInt(location.get(0))) {
                q[0] = Double.parseDouble(location.get(1));
                q[1] = Double.parseDouble(location.get(2));
                q[2] = Double.parseDouble(location.get(3));
                q[3] = Double.parseDouble(location.get(4));
                break;
            }
        }
        return q;
    }

    public abstract int getLayer(String cameraId, String imageId);

    /**
     * Created by mpowell on 5/3/14.
     */
    public abstract static class MER extends Rover {

        public static final String COURSE = "Course";

        enum TitleState {
            START,
            SOL_NUMBER,
            IMAGESET_ID,
            INSTRUMENT_NAME,
            MARS_LOCAL_TIME,
            DISTANCE,
            YAW,
            PITCH,
            ROLL,
            TILT,
            ROVER_MOTION_COUNTER
        }

        @Override
        public String getCameraId(String imageId) {
            if (imageId.contains("Sol")) { //Color Pancam image IDs begin with "Sol"...
                return "P";
            }
            return imageId.substring(1,2);
        }

        public int getLayer(String cameraId, String imageId) {
            if (cameraId.charAt(0) == 'N') {
                return 3;
            } else if (!imageId.contains("Sol")) {
                return 2;
            }
            return 1;
        }

        @Override
        public double getCameraFOV(String cameraId) {
            char camera = cameraId.charAt(0);
            if (camera == 'N')
                return 0.78539816;
            else if (camera == 'P')
                return 0.27925268;
            Log.e(TAG, "Unexpected camera id for FOV check: "+cameraId);
            return 0;
        }

        public float getMastX() {
            return 0.456f;
        }

        public float getMastY() {
            return 0.026f;
        }

        public float getMastZ() {
            return -1.0969f;
        }

        @Override
        public String getSortableImageFilename(String sourceURL) {
            String[] tokens = sourceURL.split("/");
            String filename = tokens[tokens.length - 1];
            if (filename.startsWith("Sol"))
                return "0"; //sort Cornell Pancam images first
            else if ((filename.startsWith("1") || filename.startsWith("2")) && filename.length() == 31)
                return filename.substring(23);

            return filename;
        }

        @Override
        public String getCaptionText(Note note) {
            if (note == null) return "";
            MERTitle title = tokenize(note.getTitle());
            if (title.distance == 0.0f)
                return String.format("%s image taken on Sol %d.", title.instrumentName, title.sol);
            else
                return String.format("Drive for %.2f meters on Sol %d.", title.distance, title.sol);
        }

        @Override
        public String getLabelText(Note note) {
            if (note == null) return "";
            MERTitle merTitle = tokenize(note.getTitle());
            if (merTitle.distance == 0f)
                return merTitle.instrumentName;
            else
                return String.format("Drive for %.2f meters", merTitle.distance);
        }

        @Override
        public String getDetailText(Note note) {
            if (note == null) return "";
            String marstime = tokenize(note.getTitle()).marsLocalTime;
            return (marstime != null) ? marstime + " LST" : "";
        }

        @Override
        public int getSol(Note note) {
            if (note == null) return 0;
            String title = note.getTitle();
            String tokens[] = title.split(" ");
            if (tokens.length >= 2) {
                return Integer.parseInt(tokens[1]);
            }
            return 0;
        }

        public MERTitle tokenize(String title) {
            MERTitle mer = new MERTitle();
            String[] tokens = title.split(" ");
            TitleState state = TitleState.START;
            for (String word : tokens) {
                switch (word) {
                    case SOL:
                        state = TitleState.SOL_NUMBER;
                        continue;
                    case LTST:
                        state = TitleState.MARS_LOCAL_TIME;
                        continue;
                    case RMC:
                        state = TitleState.ROVER_MOTION_COUNTER;
                        continue;
                }
                String[] indices;
                switch (state) {
                    case START:
                        break;
                    case SOL_NUMBER:
                        mer.sol = Integer.parseInt(word);
                        state = TitleState.IMAGESET_ID;
                        break;
                    case IMAGESET_ID:
                        if (word.equals(COURSE)) {
                            mer = parseCoursePlotTitle(title, mer);
                            return mer;
                        } else {
                            mer.imageSetID = word;
                        }
                        state = TitleState.INSTRUMENT_NAME;
                        break;
                    case INSTRUMENT_NAME:
                        if (mer.instrumentName == null) {
                            mer.instrumentName = word;
                        } else {
                            mer.instrumentName += " " + word;
                        }
                        break;
                    case MARS_LOCAL_TIME:
                        mer.marsLocalTime = word;
                        break;
                    case ROVER_MOTION_COUNTER:
                        indices = word.split("-");
                        mer.siteIndex = Integer.parseInt(indices[0]);
                        mer.driveIndex = Integer.parseInt(indices[1]);
                        break;
                    default:
                        Log.w("mer title", "Unexpected state in parsing image title: " + state);
                        break;
                }
            }
            return mer;
        }

        public MERTitle parseCoursePlotTitle(String title, MERTitle mer) {
            String[] tokens = title.split(" ");
            TitleState state = TitleState.START;
            for (String word : tokens) {
                switch (word) {
                    case COURSE:
                        mer.instrumentName = "Course Plot";
                        break;
                    case "Distance":
                        state = TitleState.DISTANCE;
                        continue;
                    case "yaw":
                        state = TitleState.YAW;
                        continue;
                    case "pitch":
                        state = TitleState.PITCH;
                        continue;
                    case "roll":
                        state = TitleState.ROLL;
                        continue;
                    case "tilt":
                        state = TitleState.TILT;
                        continue;
                    case "RMC":
                        state = TitleState.ROVER_MOTION_COUNTER;
                        continue;
                }
                String[] indices;
                switch (state) {
                    case START:
                        break;
                    case DISTANCE:
                        mer.distance = Float.parseFloat(word);
                        break;
                    case YAW:
                        mer.yaw = Float.parseFloat(word);
                        break;
                    case PITCH:
                        mer.pitch = Float.parseFloat(word);
                        break;
                    case ROLL:
                        mer.roll = Float.parseFloat(word);
                        break;
                    case TILT:
                        mer.tilt = Float.parseFloat(word);
                        break;
                    case ROVER_MOTION_COUNTER:
                        indices = word.split("-");
                        mer.siteIndex = Integer.parseInt(indices[0]);
                        mer.driveIndex = Integer.parseInt(indices[1]);
                        break;
                    default:
                        Log.w("mer title", "Unexpected state in parsing course plot title: " + state);
                        break;
                }
            }
            return mer;
        }

        @Override
        public String getImageName(Resource resource) {
            String imageid = getImageID(resource);

            if (resource.getAttributes().getSourceURL().contains("False"))
                return "Color";

            String instrument = imageid.substring(instrumentIndex, instrumentIndex + 1);
            if (instrument.equals("N") || instrument.equals("F") || instrument.equals("R")) {
                String eye = imageid.substring(eyeIndex, eyeIndex + 1);
                if (eye.equals("L"))
                    return "Left";
                else
                    return "Right";
            } else if (instrument.equals("P")) {
                return imageid.substring(eyeIndex, eyeIndex + 2);
            }

            return "";
        }

        public String[] stereoForImages(Note note) {
            if (note == null || note.getResources().size() == 0)
                return new String[0];
            String imageid = getImageID(note.getResources().get(0));
            String instrument = imageid.substring(instrumentIndex, instrumentIndex + 1);
            if (!stereoInstruments.contains(instrument) && !imageid.startsWith("Sol"))
                return new String[0];

            int leftImageIndex = -1;
            int rightImageIndex = -1;
            int index = 0;
            for (Resource resource : note.getResources()) {
                imageid = getImageID(resource);
                String eye = imageid.substring(eyeIndex, eyeIndex + 1);
                if (leftImageIndex == -1 && eye.equals("L") && !imageid.startsWith("Sol"))
                    leftImageIndex = index;
                if (rightImageIndex == -1 && eye.equals("R"))
                    rightImageIndex = index;
                index += 1;
            }
            if (leftImageIndex >= 0 && rightImageIndex >= 0) {
                return new String[]{
                        note.getResources().get(leftImageIndex).getAttributes().getSourceURL(),
                        note.getResources().get(rightImageIndex).getAttributes().getSourceURL()};
            }
            return new String[0];
        }
    }
} public abstract class AMaterial {
	public static final int NONE				= 0;
	public static final int VERTEX_ANIMATION 	= 1 << 0;
	public static final int SKELETAL_ANIMATION	= 1 << 1;
	public static final int ALPHA_MASKING		= 1 << 2;
	
	protected String mUntouchedVertexShader;
	protected String mUntouchedFragmentShader;
	protected String mVertexShader;
	protected String mFragmentShader;

	protected int mProgram;
	protected int mVShaderHandle;
	protected int mFShaderHandle;
	protected int maPositionHandle;
	protected int maTextureHandle;
	protected int maColorHandle;
	protected int maNormalHandle;
	protected int maNextFramePositionHandle;
	protected int maNextFrameNormalHandle;

	protected int muMVPMatrixHandle;
	protected int muCameraPositionHandle;
	protected int muMMatrixHandle;
	protected int muVMatrixHandle;
	protected int muInterpolationHandle;
	protected int muAlphaMaskingThresholdHandle;

	protected Stack<ALight> mLights;
	protected boolean mUseColor = false;
	protected boolean mUseAlphaMap = false;
	protected boolean mUseNormalMap = false;
	protected boolean mUseSpecMap = false;

	protected int mNumTextures = 0;
	protected float mAlphaMaskingThreshold = .5f;
	protected float[] mModelViewMatrix;
	protected float[] mViewMatrix;
	protected float[] mCameraPosArray;
	protected ArrayList<TextureInfo> mTextureInfoList;
	
	/**
	 * The maximum number of available textures for this device.
	 */
	private int mMaxTextures;
	private boolean mProgramCreated = false;
	
	protected boolean mVertexAnimationEnabled;
	protected boolean mSkeletalAnimationEnabled;
	protected boolean mAlphaMaskingEnabled;
	
	public AMaterial() {
		mTextureInfoList = new ArrayList<TextureInfo>();
		mCameraPosArray = new float[3];
		mLights = new Stack<ALight>();
		mMaxTextures = queryMaxTextures();
	}
	
	public AMaterial(String vertexShader, String fragmentShader, boolean vertexAnimationEnabled) {
		this(vertexShader, fragmentShader, vertexAnimationEnabled ? VERTEX_ANIMATION : NONE);
	}
	
	public AMaterial(String vertexShader, String fragmentShader, int parameters) {
		this();
		mUntouchedVertexShader = vertexShader;
		mUntouchedFragmentShader = fragmentShader;
		mVertexAnimationEnabled = (parameters & VERTEX_ANIMATION) != 0;
		mSkeletalAnimationEnabled = (parameters & SKELETAL_ANIMATION) != 0;
		mAlphaMaskingEnabled = (parameters & ALPHA_MASKING) != 0;
	}
	
	public AMaterial(int parameters) {
		this();
		mVertexAnimationEnabled = (parameters & VERTEX_ANIMATION) != 0;
		mSkeletalAnimationEnabled = (parameters & SKELETAL_ANIMATION) != 0;
		mAlphaMaskingEnabled = (parameters & ALPHA_MASKING) != 0;
	}
	
	public AMaterial(int vertex_res, int fragment_res) {
		this(vertex_res, fragment_res, false);
	}
	
	public AMaterial(int vertex_res, int fragment_res, boolean vertexAnimationEnabled) {
		this(RawMaterialLoader.fetch(vertex_res), RawMaterialLoader.fetch(fragment_res), vertexAnimationEnabled);
	}
	
	public AMaterial(int vertex_res, int fragment_res, int parameters) {
		this(RawMaterialLoader.fetch(vertex_res), RawMaterialLoader.fetch(fragment_res), parameters);
	}
	
	protected int queryMaxTextures() {
		int numTexUnits[] = new int[1];
		GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_IMAGE_UNITS, numTexUnits, 0);
		return numTexUnits[0];
	}
	
	public void reload() {
		setShaders(mUntouchedVertexShader, mUntouchedFragmentShader);
		
		mNumTextures = mTextureInfoList.size();

		for(int i=0; i<mNumTextures; i++) {
			if(mTextureInfoList.get(i).getTexture() != null)
				addTexture(mTextureInfoList.get(i), true, true);
		}
	}

	public void setShaders() {
		setShaders(mUntouchedVertexShader, mUntouchedFragmentShader);
	}
	
	public void setShaders(String vertexShader, String fragmentShader) {
		mVertexShader = mVertexAnimationEnabled ? "#define VERTEX_ANIM\n" + vertexShader : vertexShader;
		mVertexShader = mSkeletalAnimationEnabled ? "#define SKELETAL_ANIM\n" + mVertexShader : mVertexShader;
		mVertexShader = mUseColor ? mVertexShader : "#define TEXTURED\n" + mVertexShader;
		mFragmentShader = mUseColor ? fragmentShader : "#define TEXTURED\n" + fragmentShader;
		mFragmentShader = mAlphaMaskingEnabled ? "#define ALPHA_MASK\n" + mFragmentShader : mFragmentShader;
		mFragmentShader = mUseAlphaMap ? "#define ALPHA_MAP\n" + mFragmentShader : mFragmentShader;
		mFragmentShader = mUseNormalMap ? "#define NORMAL_MAP\n" + mFragmentShader : mFragmentShader;
		mFragmentShader = mUseSpecMap ? "#define SPECULAR_MAP\n" + mFragmentShader : mFragmentShader;

		if(RajawaliRenderer.isFogEnabled())
		{
			mVertexShader = "#define FOG_ENABLED\n" + mVertexShader;
			mFragmentShader = "#define FOG_ENABLED\n" + mFragmentShader;
		}

		mProgram = createProgram(mVertexShader, mFragmentShader);
		if (mProgram == 0)
			return;

		maPositionHandle = getAttribLocation("aPosition");
		maNormalHandle = getAttribLocation("aNormal");
		maTextureHandle = getAttribLocation("aTextureCoord");
		maColorHandle = getAttribLocation("aColor");
		
		muCameraPositionHandle = getUniformLocation("uCameraPosition");
		muMVPMatrixHandle = getUniformLocation("uMVPMatrix");
		muMMatrixHandle = getUniformLocation("uMMatrix");
		muVMatrixHandle = getUniformLocation("uVMatrix");
		
		if(mVertexAnimationEnabled == true) {
			maNextFramePositionHandle = getAttribLocation("aNextFramePosition");
			maNextFrameNormalHandle = getAttribLocation("aNextFrameNormal");
			muInterpolationHandle = getUniformLocation("uInterpolation");
		}
		
		if(mAlphaMaskingEnabled == true) {
			muAlphaMaskingThresholdHandle = getUniformLocation("uAlphaMaskingThreshold");
		}
		
		mProgramCreated = true;

		checkTextureHandles();
	}

	protected int loadShader(int shaderType, String source) {
		int shader = GLES20.glCreateShader(shaderType);
		if (shader != 0) {
			GLES20.glShaderSource(shader, source);
			GLES20.glCompileShader(shader);
			int[] compiled = new int[1];
			GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
			if (compiled[0] == 0) {
				RajLog.e("[" +getClass().getName()+ "] Could not compile " + (shaderType == GLES20.GL_FRAGMENT_SHADER ? "fragment" : "vertex") + " shader:");
				RajLog.e("Shader log: " + GLES20.glGetShaderInfoLog(shader));
				GLES20.glDeleteShader(shader);
				shader = 0;
			}
		}
		return shader;
	}

	protected int createProgram(String vertexSource, String fragmentSource) {
		mVShaderHandle = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
		if (mVShaderHandle == 0) {
			return 0;
		}

		mFShaderHandle = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
		if (mFShaderHandle == 0) {
			return 0;
		}

		int program = GLES20.glCreateProgram();
		if (program != 0) {
			GLES20.glAttachShader(program, mVShaderHandle);
			GLES20.glAttachShader(program, mFShaderHandle);
			GLES20.glLinkProgram(program);

			int[] linkStatus = new int[1];
			GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
			if (linkStatus[0] != GLES20.GL_TRUE) {
				RajLog.e("Could not link program in " + getClass().getCanonicalName() +": ");
				RajLog.e(GLES20.glGetProgramInfoLog(program));
				RajLog.d("-=-=-= VERTEX SHADER =-=-=-");
				RajLog.d(mVertexShader);
				RajLog.d("-=-=-= FRAGMENT SHADER =-=-=-");
				RajLog.d(mFragmentShader);
				GLES20.glDeleteProgram(program);
				program = 0;
			}
		}
		return program;
	}

	protected int getUniformLocation(String name) {
		return GLES20.glGetUniformLocation(mProgram, name);
	}

	protected int getAttribLocation(String name) {
		return GLES20.glGetAttribLocation(mProgram, name);
	}
	
	public void unload() {
		GLES20.glDeleteShader(mVShaderHandle);
		GLES20.glDeleteShader(mFShaderHandle);
		GLES20.glDeleteProgram(mProgram);
	}
	
	public void destroy() {
		mModelViewMatrix = null;
		mViewMatrix = null;
		mCameraPosArray = null;
		if(mLights != null) mLights.clear();
		if(mTextureInfoList != null) mTextureInfoList.clear();
		unload();
	}

	public void useProgram() {
		if(!mProgramCreated) {
			mMaxTextures = queryMaxTextures();
			reload();
		}
		GLES20.glUseProgram(mProgram);
		if(checkValidHandle(muAlphaMaskingThresholdHandle, "alpha masking threshold"))
			GLES20.glUniform1f(muAlphaMaskingThresholdHandle, mAlphaMaskingThreshold);
	}

	public void bindTextures() {
		int num = mTextureInfoList.size();

		for (int i = 0; i < num; i++) {
			TextureInfo ti = mTextureInfoList.get(i);
			int type = ti.isCubeMap() ? GLES20.GL_TEXTURE_CUBE_MAP : GLES20.GL_TEXTURE_2D;
			GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + i);
			GLES20.glBindTexture(type, ti.getTextureId());
			GLES20.glUniform1i(ti.getUniformHandle(), i);
		}
	}

	public void unbindTextures() {
		int num = mTextureInfoList.size();

		for (int i = 0; i < num; i++) {
			TextureInfo ti = mTextureInfoList.get(i);
			int type = ti.isCubeMap() ? GLES20.GL_TEXTURE_CUBE_MAP
					: GLES20.GL_TEXTURE_2D;
			GLES20.glBindTexture(type, 0);
		}
		
		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
	}

	public ArrayList<TextureInfo> getTextureInfoList() {
		return mTextureInfoList;
	}
	
	public void setTextureInfoList(ArrayList<TextureInfo> textureInfoList) {
		mTextureInfoList = textureInfoList;
	}

	public void addTexture(TextureInfo textureInfo) {
		addTexture(textureInfo, false);
	}
	
	public void addTexture(TextureInfo textureInfo, boolean isExistingTexture) {
		addTexture(textureInfo, isExistingTexture, false);
	}
	
	public void removeTexture(TextureInfo textureInfo) {
		mTextureInfoList.remove(textureInfo);
	}
	
	public void addTexture(TextureInfo textureInfo, boolean isExistingTexture, boolean reload) {
		// -- check if this texture is already in the list
		if(mTextureInfoList.indexOf(textureInfo) > -1 && !reload) return;		

		if(mTextureInfoList.size() > mMaxTextures) {
			RajLog.e("[" +getClass().getCanonicalName()+ "] Maximum number of textures for this material has been reached. Maximum number of textures is " + mMaxTextures + ".");
		}
		
		String textureName = "uTexture";

		switch (textureInfo.getTextureType()) {
		case DIFFUSE:
		case VIDEO_TEXTURE:
			textureName = "uDiffuseTexture";
			break;
		case BUMP:
			textureName = "uNormalTexture";
			mUseNormalMap = true;
			break;
		case SPECULAR:
			textureName = "uSpecularTexture";
			mUseSpecMap = true;
			break;
		case ALPHA:
			textureName = "uAlphaTexture";
			mUseAlphaMap = true;
			break;
		case FRAME_BUFFER:
			textureName = "uFrameBufferTexture";
			break;
		case DEPTH_BUFFER:
			textureName = "uDepthBufferTexture";
			break;
		case LOOKUP:
			textureName = "uLookupTexture";
			break;
		case CUBE_MAP:
			textureName = "uCubeMapTexture";
			break;
		case SPHERE_MAP:
			textureName = "uSphereMapTexture";
			break;
		}

		// -- check if there are already diffuse texture in the list
		int num = mTextureInfoList.size();
		int numDiffuse = 0;
		for(int i=0; i<num; ++i) {
			TextureInfo ti = mTextureInfoList.get(i);
			if(ti.getTextureType() == TextureType.DIFFUSE)
				numDiffuse++;
		}
		
		// -- if there are already diffuse textures in the list then append a
		//    number (ie the second texture in the list will be called 
		//    "uDiffuseTexture1", the third "uDiffuseTexture2", etc.
		if(numDiffuse > 0 && textureInfo.getTextureType() == TextureType.DIFFUSE)
			textureName += numDiffuse;

		if(isExistingTexture)
			textureName = textureInfo.getTextureName();
		
		if(mProgramCreated) {
			int textureHandle = GLES20.glGetUniformLocation(mProgram, textureName);
			if (textureHandle == -1) {
				RajLog.d("Could not get attrib location for "
						+ textureName + ", " + textureInfo.getTextureType());
			}
			textureInfo.setUniformHandle(textureHandle);
		}
		
		if(!isExistingTexture)
			textureInfo.setTextureName(textureName);
		
		if(textureInfo.getTextureType() != TextureType.SPHERE_MAP) mUseColor = false;
		if(!isExistingTexture) {
			mTextureInfoList.add(textureInfo);
			mNumTextures++;
		}
	}
	
	protected void checkTextureHandles() {
		int num = mTextureInfoList.size();
		for(int i=0; i<num; ++i) {
			TextureInfo ti = mTextureInfoList.get(i);
			if(ti.getUniformHandle() == -1) {
				addTexture(ti, true, true);
			}
		}
	}
	
	public void setVertices(final int vertexBufferHandle) {
		if(checkValidHandle(vertexBufferHandle, "vertex data")){
			GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBufferHandle);
			GLES20.glEnableVertexAttribArray(maPositionHandle);
			GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT,
					false, 0, 0);
		}
	}

	public void setTextureCoords(int textureCoordBufferHandle) {
		if(checkValidHandle(textureCoordBufferHandle, "texture coordinates"))
			setTextureCoords(textureCoordBufferHandle, false);
	}

	public void setTextureCoords(final int textureCoordBufferHandle,
			boolean hasCubemapTexture) {
		if(checkValidHandle(textureCoordBufferHandle, "texture coordinates"))
		{
			GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, textureCoordBufferHandle);
			GLES20.glEnableVertexAttribArray(maTextureHandle);
			GLES20.glVertexAttribPointer(maTextureHandle,
					hasCubemapTexture ? 3 : 2, GLES20.GL_FLOAT, false, 0, 0);
		}
	}

	public void setColors(final int colorBufferHandle) {
		if(checkValidHandle(colorBufferHandle, "color data"))
		{
			GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, colorBufferHandle);
			GLES20.glEnableVertexAttribArray(maColorHandle);
			GLES20.glVertexAttribPointer(maColorHandle, 4, GLES20.GL_FLOAT,
					false, 0, 0);
		}
	}

	public void setNormals(final int normalBufferHandle) {
		if(checkValidHandle(normalBufferHandle, "normal data"))
			if(checkValidHandle(maNormalHandle, null)){
			GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, normalBufferHandle);
			GLES20.glEnableVertexAttribArray(maNormalHandle);
			GLES20.glVertexAttribPointer(maNormalHandle, 3, GLES20.GL_FLOAT,
					false, 0, 0);
		}
	}

	public void setMVPMatrix(float[] mvpMatrix) {
		if(checkValidHandle(muMVPMatrixHandle, "mvp matrix"))
			GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mvpMatrix, 0);

	}

	public void setModelMatrix(float[] modelMatrix) {
		mModelViewMatrix = modelMatrix;
		if(checkValidHandle(muMMatrixHandle, null))
			GLES20.glUniformMatrix4fv(muMMatrixHandle, 1, false, modelMatrix, 0);
	}

	public void setViewMatrix(float[] viewMatrix) {
		mViewMatrix = viewMatrix;
		if(checkValidHandle(muVMatrixHandle, null))
			GLES20.glUniformMatrix4fv(muVMatrixHandle, 1, false, viewMatrix, 0);
	}
	
	public void setInterpolation(float interpolation) {
		if(checkValidHandle(muInterpolationHandle, "interpolation"))
			GLES20.glUniform1f(muInterpolationHandle, interpolation);
	}
	
	public void setNextFrameVertices(final int vertexBufferHandle) {
		if(checkValidHandle(vertexBufferHandle, "NextFrameVertices")){
			if(checkValidHandle(maNextFramePositionHandle, "maNextFramePositionHandle")){
				GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBufferHandle);
				GLES20.glEnableVertexAttribArray(maNextFramePositionHandle);
				GLES20.glVertexAttribPointer(maNextFramePositionHandle, 3, GLES20.GL_FLOAT,
						false, 0, 0);
			}
		}
	}
	
	public void setNextFrameNormals(final int normalBufferHandle) {
		if(checkValidHandle(normalBufferHandle, "NextFrameNormals")){
			if(checkValidHandle(maNextFrameNormalHandle, "maNextFrameNormalHandle")){
				GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, normalBufferHandle);
				GLES20.glEnableVertexAttribArray(maNextFrameNormalHandle);
				GLES20.glVertexAttribPointer(maNextFrameNormalHandle, 3, GLES20.GL_FLOAT,
						false, 0, 0);
			}
		}
	}
	
	/**
	 * Set the threshold for alpha masking. The default value is .5f
	 * 
	 * 
	 * @param threshold Pixels with alpha values below this number will be discarded (range 0 - 1)
	 */
	
	public void setAlphaMaskingThreshold(float threshold) {
		mAlphaMaskingThreshold = threshold;
	}
	
	public boolean checkValidHandle(int handle, String message){
		if(handle >= 0)
			return true;
		if(message != null)
			RajLog.e("[" +getClass().getCanonicalName()+ "] Trying to set "+message+
				" without a valid handle.");
		return false;					
	}
	
	public void setLightParams() {
		
	}
	
	public void setLights(List<ALight> lights) {
		if(lights == null || lights.size() == 0)
			return;
		for(int i=0; i<lights.size(); ++i) {
			if(i>=mLights.size()) 
				mLights.add(lights.get(i));
			else
				mLights.set(i, lights.get(i));
		}
	}
	
	public void setCamera(Camera camera) {
		Number3D camPos = camera.getPosition();
		mCameraPosArray[0] = camPos.x;
		mCameraPosArray[1] = camPos.y;
		mCameraPosArray[2] = camPos.z;
		if (muCameraPositionHandle > -1)
			GLES20.glUniform3fv(muCameraPositionHandle, 1, mCameraPosArray, 0);
	}

	public String toString() {
		StringBuffer out = new StringBuffer();
		out.append("[" +getClass().getName()+ "]\n");
		out.append("program: ").append(mProgram).append("\n");
		out.append("vshader handle: ").append(mVShaderHandle).append("\n");
		out.append("fshader handle: ").append(mFShaderHandle).append("\n");
		out.append("program created: ").append(mProgramCreated).append("\n");
		return out.toString();
	}
	
	/**
	 * Get the model-space to view-space matrix
	 * 
	 * @return {@link float[]}
	 */

	public float[] getModelViewMatrix() {
		return mModelViewMatrix;
	}

	public void copyTexturesTo(AMaterial shader) {
		int num = mTextureInfoList.size();

		for (int i = 0; i < num; ++i)
			shader.addTexture(mTextureInfoList.get(i));
	}

	/**
	 * Set the material to use a color value rather than a texture
	 * 
	 * @param value {@link boolean}
	 */
	public void setUseColor(boolean value) {
		if(value != mUseColor) {
			mUseColor = value;
			if(mLights.size() > 0 || !(this instanceof AAdvancedMaterial))
				setShaders(mUntouchedVertexShader, mUntouchedFragmentShader);
		}
		mUseColor = value;
	}
	
	/**
	 *  Determine if color is used.
	 *  
	 * @return {@link boolean}
	 */
	public boolean getUseColor() {
		return mUseColor;
	}
		
	/**
	 * Pass the context to be used for resource loading. This should only be called internally by the renderer.
	 * 
	 * This is kind of a hack but I do not see any better way to load resources for materials.
	 * 
	 * @param context
	 */
	public static final void setLoaderContext(final Context context) {
		RawMaterialLoader.mContext = new WeakReference<Context>(context);
	}
	
	/**
	 * Internal class for managing shader loading.
	 * 
	 * This class is mostly internal unfortunately loading of resources requires context so this class has help from
	 * AMaterial to statically pass the context and store it as a weak reference. Unfortunately there is no way around
	 * this that I can see but I am open for suggestions of a better solution.
	 * 
	 * @author Ian Thomas (toxicbakery aka damnusernames http://toxicbakery.com/)
	 * 
	 */
	protected static final class RawMaterialLoader {

		@SuppressLint("UseSparseArrays")
		private static final HashMap<Integer, String> mRawMaterials = new HashMap<Integer, String>();

		// Prevent memory leaks as referencing the context can be dangerous.
		public static WeakReference<Context> mContext;

		/**
		 * Read a material from the raw resources folder. Subsequent calls will return from memory.
		 * 
		 * @param resID
		 * @return
		 */
		public static final String fetch(final int resID) {
			if (mRawMaterials.containsKey(resID))
				return mRawMaterials.get(resID);

			final StringBuilder sb = new StringBuilder();
			
			try {
				final Resources res = mContext.get().getResources();
				final InputStreamReader isr = new InputStreamReader(res.openRawResource(resID));
				final BufferedReader br = new BufferedReader(isr);
				
				String line;
				while ((line = br.readLine()) != null)
					sb.append(line).append("\n");
				
				mRawMaterials.put(resID, sb.toString());
				
				isr.close();
				br.close();
			} catch (Exception e) {
				RajLog.e("Failed to read material: " + e.getMessage());
				e.printStackTrace();
			}

			return mRawMaterials.get(resID);
		}
	}
} public class DiffuseMaterial extends AAdvancedMaterial {
	
	public DiffuseMaterial() {
		this(false);
	}
	
	public DiffuseMaterial(String vertexShader, String fragmentShader, boolean isAnimated) {
		super(vertexShader, fragmentShader, isAnimated);
	}
	
	public DiffuseMaterial(int vertex_resID, int fragment_resID, boolean isAnimated) {
		super(vertex_resID, fragment_resID, isAnimated);
	}
	
	public DiffuseMaterial(boolean isAnimated) {
		this(R.raw.diffuse_material_vertex, R.raw.diffuse_material_fragment, isAnimated);
	}
	
	public DiffuseMaterial(int parameters) {
		super(R.raw.diffuse_material_vertex, R.raw.diffuse_material_fragment, parameters);
	}
	
	public DiffuseMaterial(String vertexShader, String fragmentShader) {
		super(vertexShader, fragmentShader);
	}
	
	public void setShaders(String vertexShader, String fragmentShader) {
		StringBuffer fc = new StringBuffer();
		StringBuffer vc = new StringBuffer();

		for(int i=0; i<mLights.size(); ++i) {
			ALight light = mLights.get(i);
			
			if(light.getLightType() == ALight.POINT_LIGHT) {
				vc.append("dist = distance(V.xyz, uLightPosition").append(i).append(");\n");
				vc.append("vAttenuation").append(i).append(" = 1.0 / (uLightAttenuation").append(i).append("[1] + uLightAttenuation").append(i).append("[2] * dist + uLightAttenuation").append(i).append("[3] * dist * dist);\n");
				fc.append("L = normalize(uLightPosition").append(i).append(" - V.xyz);\n");
			} else if(light.getLightType() == ALight.SPOT_LIGHT) {
				vc.append("dist = distance(V.xyz, uLightPosition").append(i).append(");\n");
				vc.append("vAttenuation").append(i).append(" = (uLightAttenuation").append(i).append("[1] + uLightAttenuation").append(i).append("[2] * dist + uLightAttenuation").append(i).append("[3] * dist * dist);\n");
				fc.append("L = normalize(uLightPosition").append(i).append(" - V.xyz);\n");
				fc.append("vec3 spotDir").append(i).append(" = normalize(-uLightDirection").append(i).append(");\n");
				fc.append("float spot_factor").append(i).append(" = dot( L, spotDir").append(i).append(" );\n");
				fc.append("if( uSpotCutoffAngle").append(i).append(" < 180.0 ) {\n");
					fc.append("if( spot_factor").append(i).append(" >= cos( radians( uSpotCutoffAngle").append(i).append(") ) ) {\n");
						fc.append("spot_factor").append(i).append(" = (1.0 - (1.0 - spot_factor").append(i).append(") * 1.0/(1.0 - cos( radians( uSpotCutoffAngle").append(i).append("))));\n");
						fc.append("spot_factor").append(i).append(" = pow(spot_factor").append(i).append(", uSpotFalloff").append(i).append("* 1.0/spot_factor").append(i).append(");\n");
					fc.append("}\n");
					fc.append("else {\n");
						fc.append("spot_factor").append(i).append(" = 0.0;\n");
					fc.append("}\n");
					fc.append("L = vec3(L.y, L.x, L.z);\n");
					fc.append("}\n");
			} else if(light.getLightType() == ALight.DIRECTIONAL_LIGHT) {
				vc.append("vAttenuation").append(i).append(" = 1.0;\n");
				fc.append("L = normalize(-uLightDirection").append(i).append(");\n");
			}

			fc.append("NdotL = max(dot(N, L), 0.1);\n");
			fc.append("power = uLightPower").append(i).append(" * NdotL * vAttenuation").append(i).append(";\n");
			fc.append("intensity += power;\n"); 
			
			if(light.getLightType() == ALight.SPOT_LIGHT)
				fc.append("Kd.rgb += uLightColor").append(i).append(" * spot_factor").append(i).append(";\n");
			else
				fc.append("Kd.rgb += uLightColor").append(i).append(" * power;\n");
		}
		
		super.setShaders(vertexShader.replace("%LIGHT_CODE%", vc.toString()), fragmentShader.replace("%LIGHT_CODE%", fc.toString()));
	}
} public class SimpleMaterial extends AMaterial {

	public SimpleMaterial() {
		super(R.raw.simple_material_vertex, R.raw.simple_material_fragment, false);
	}
	
	/**
	 * Constructor to pass parameters directly
	 * 
	 * @param parameters Use bitwise parameters from `AMaterial`
	 */
	public SimpleMaterial(int parameters) {
		super(R.raw.simple_material_vertex, R.raw.simple_material_fragment, parameters);
	}
	
	public SimpleMaterial(int vertex_resID, int fragment_resID) {
		super(vertex_resID, fragment_resID, false);
	}

	public SimpleMaterial(String vertexShader, String fragmentShader) {
		super(vertexShader, fragmentShader, false);
	}

	public void setShaders(String vertexShader, String fragmentShader) {
		super.setShaders(vertexShader, fragmentShader);
	}
} public class TextureInfo {
	/**
	 * This texture's unique id
	 */
	protected int mTextureId;
	/**
	 * The type of texture
	 * 
	 * @see TextureManager.TextureType
	 */
	protected TextureType mTextureType;
	protected String mTextureName = "";
	/**
	 * The shader uniform handle for this texture
	 */
	protected int mUniformHandle = -1;
	/**
	 * Texture width
	 */
	protected int mWidth;
	/**
	 * Texture height
	 */
	protected int mHeight;
	protected Bitmap mTexture;
	protected Bitmap[] mTextures;
	protected boolean mMipmap;
	protected Config mBitmapConfig;
	protected boolean mShouldRecycle;
	
	protected CompressionType mCompressionType;
	protected int mInternalFormat;
	protected ByteBuffer[] mBuffer;
	
	/**
	 * The type of texture
	 * 
	 * @see TextureManager.WrapType
	 */
	protected WrapType mWrapType;
	/**
	 * The type of texture
	 * 
	 * @see TextureManager.FilterType
	 */
	protected FilterType mFilterType;
	
	protected boolean isCubeMap = false;
	
	/**
	 * OpenGL bitmap format
	 */
	protected int mBitmapFormat;

	public TextureInfo(TextureInfo other) {
		setFrom(other);
	}
	
	public TextureInfo(int textureId) {
		this(textureId, TextureType.DIFFUSE);
	}

	public TextureInfo(int textureId, TextureType textureType) {
		mTextureId = textureId;
		mTextureType = textureType;
	}
	
	public void setFrom(TextureInfo other) {
		mTextureId = other.getTextureId();
		mTextureType = other.getTextureType();
		mUniformHandle = other.getUniformHandle();
		mWidth = other.getWidth();
		mHeight = other.getHeight();
		mTexture = other.getTexture();
		mTextures = other.getTextures();
		mMipmap = other.isMipmap();
		mBitmapConfig = other.getBitmapConfig();
		mTextureName = other.getTextureName();
		mInternalFormat = other.getInternalFormat();
		mCompressionType = other.getCompressionType();
		mBuffer = other.getBuffer();
	}
	
	public void setTextureId(int id) {
		mTextureId = id;
	}

	public int getTextureId() {
		return mTextureId;
	}
	
	public void setTextureName(String name) {
		mTextureName = name;
	}
	
	public String getTextureName() {
		return mTextureName;
	}

	public void setUniformHandle(int handle) {
		mUniformHandle = handle;
	}

	public int getUniformHandle() {
		return mUniformHandle;
	}
	
	public boolean isCubeMap() {
		return isCubeMap;
	}
	
	public void setIsCubeMap(boolean cube) {
		isCubeMap = cube;
	}

	public String toString() {
		return "id: " + mTextureId + " handle: " + mUniformHandle + " type: " + mTextureType + " name: " + mTextureName;
	}

	public TextureType getTextureType() {
		return mTextureType;
	}

	public void setTextureType(TextureType textureType) {
		this.mTextureType = textureType;
	}

	public int getWidth() {
		return mWidth;
	}

	public void setWidth(int width) {
		this.mWidth = width;
	}

	public int getHeight() {
		return mHeight;
	}

	public void setHeight(int height) {
		this.mHeight = height;
	}

	public Bitmap getTexture() {
		return mTexture;
	}

	public void setTexture(Bitmap texture) {
		this.mTexture = texture;
	}

	public Bitmap[] getTextures() {
		return mTextures;
	}

	public void setTextures(Bitmap[] textures) {
		this.mTextures = textures;
	}

	public boolean isCompressed() {
		return mCompressionType != CompressionType.NONE;
	}
	
	public int getInternalFormat() {
		return mInternalFormat;
	}
	
	public void setInternalFormat(int internalformat) {
		this.mInternalFormat = internalformat;
	}
	
	public CompressionType getCompressionType() {
		return this.mCompressionType;
	}
	
	public void setCompressionType(CompressionType compressionType) {
		this.mCompressionType = compressionType;
	}

	public ByteBuffer[] getBuffer() {
		return this.mBuffer;
	}
	
	public void setBuffer(ByteBuffer[] buffer) {
		this.mBuffer = buffer;
	}
	
	public boolean isMipmap() {
		return mMipmap;
	}

	public void setMipmap(boolean mipmap) {
		this.mMipmap = mipmap;
	}

	public Config getBitmapConfig() {
		return mBitmapConfig;
	}

	public void setBitmapConfig(Config bitmapConfig) {
		this.mBitmapConfig = bitmapConfig;
	}
	
	public void setFilterType(FilterType filterType) {
		this.mFilterType = filterType;
	}
	
	public FilterType getFilterType() {
		return mFilterType;
	}
	
	public void setWrapType(WrapType wrapType) {
		this.mWrapType = wrapType;
	}
	
	public WrapType getWrapType() {
		return mWrapType;
	}
	
	public boolean shouldRecycle() {
		return mShouldRecycle;
	}
	
	public void shouldRecycle(boolean should) {
		mShouldRecycle = should;
	}
} public class TextureManager {
	private Context mContext;
	private boolean mShouldValidateTextures;
	private TextureInfo mCurrentValidatingTexInfo;
	private Stack<TextureInfo> mTexturesToUpdate;
	private boolean mShouldUpdateTextures;
	private static final int GL_TEXTURE_EXTERNAL_OES = 0x8D65;
	
	// Paletted texture constants
	// Referenced from OpenGL ES 2.0 extension C header from Khronos Group
	// http://www.khronos.org/registry/gles/api/2.0/gl2ext.h
	private static final int GL_PALETTE4_RGB8_OES					= 0x8B90;
	private static final int GL_PALETTE4_RGBA8_OES					= 0x8B91;
	private static final int GL_PALETTE4_R5_G6_B5_OES				= 0x8B92;
	private static final int GL_PALETTE4_RGBA4_OES					= 0x8B93;
	private static final int GL_PALETTE4_RGB5_A1_OES				= 0x8B94;
	private static final int GL_PALETTE8_RGB8_OES					= 0x8B95;
	private static final int GL_PALETTE8_RGBA8_OES					= 0x8B96;
	private static final int GL_PALETTE8_R5_G6_B5_OES				= 0x8B97;
	private static final int GL_PALETTE8_RGBA4_OES					= 0x8B98;
	private static final int GL_PALETTE8_RGB5_A1_OES				= 0x8B99;
	
	// PowerVR Texture compression constants
	private static final int GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG		= 0x8C00;
	private static final int GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG		= 0x8C01;
	private static final int GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG	= 0x8C02;
	private static final int GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG	= 0x8C03;

	// S3 texture compression constants
	private static final int GL_COMPRESSED_RGB_S3TC_DXT1_EXT		= 0x83F0;
	private static final int GL_COMPRESSED_RGBA_S3TC_DXT1_EXT		= 0x83F1;
	
	/**
	 * List containing texture information objects
	 */
	private List<TextureInfo> mTextureInfoList;
	/**
	 * Cube map faces
	 */
	private final int[] CUBE_FACES = new int[] {
			GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_X,
			GLES20.GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
			GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
			GLES20.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
			GLES20.GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
			GLES20.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z
	};
	/**
	 * Texture types. Can be DIFFUSE, BUMP, FRAME_BUFFER, DEPTH_BUFFER, LOOKUP, CUBE_MAP
	 */
	public enum TextureType {
		DIFFUSE,
		BUMP,
		SPECULAR,
		ALPHA,
		FRAME_BUFFER,
		DEPTH_BUFFER,
		LOOKUP,
		CUBE_MAP,
		SPHERE_MAP,
		VIDEO_TEXTURE
	};
	
	public enum CompressionType {
		NONE,
		ETC1,
		PALETTED,
		THREEDC,
		ATC,
		DXT1,
		PVRTC
	};
	
	public enum WrapType {
		CLAMP,
		REPEAT		
	};
	
	public enum FilterType{
		NEAREST,
		LINEAR
	};
	
	public enum PaletteFormat {
		PALETTE4_RGB8,
		PALETTE4_RGBA8,
		PALETTE4_R5_G6_B5,
		PALETTE4_RGBA4,
		PALETTE4_RGB5_A1,
		PALETTE8_RGB8,
		PALETTE8_RGBA8,
		PALETTE8_R5_G6_B5,
		PALETTE8_RGBA4,
		PALETTE8_RGB5_A1
	};
	
	public enum ThreeDcFormat {
		X,
		XY
	};
	
	public enum AtcFormat {
		RGB,
		RGBA_EXPLICIT,
		RGBA_INTERPOLATED
	};
	
	public enum Dxt1Format {
		RGB,
		RGBA
	};
	
	public enum PvrtcFormat {
		RGB_2BPP,
		RGB_4BPP,
		RGBA_2BPP,
		RGBA_4BPP
	};
	
	public TextureManager(Context context) {
		mTextureInfoList = Collections.synchronizedList(new CopyOnWriteArrayList<TextureInfo>()); 
		mTexturesToUpdate = new Stack<TextureInfo>();
		mContext = context;
	}
	
	public TextureInfo addTexture(int resID) {
		return addTexture(getBitmap(resID));
	}
	public TextureInfo addTexture(int resID, TextureType textureType) {
		return this.addTexture(getBitmap(resID), textureType, true, false, WrapType.REPEAT, FilterType.LINEAR);	
	}
	
	public TextureInfo addTexture(int resID, TextureType textureType, boolean mipmap) {
	    return this.addTexture(getBitmap(resID), textureType, mipmap, false, WrapType.REPEAT, FilterType.LINEAR);
	}

	public TextureInfo addTexture(int resID, boolean mipmap, boolean recycle) {
	    return this.addTexture(getBitmap(resID), TextureType.DIFFUSE, mipmap, recycle, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	public TextureInfo addTexture(int resID, TextureType textureType, WrapType wrapType, FilterType filterType) {
		return this.addTexture(getBitmap(resID), textureType, true, false, wrapType, filterType);	
	}

	public TextureInfo addTexture(int resID, TextureType textureType, boolean mipmap, boolean recycle) {
		return this.addTexture(getBitmap(resID), textureType, mipmap, recycle, WrapType.REPEAT, FilterType.LINEAR);
	}
	public TextureInfo addTexture(int resID, TextureType textureType, boolean mipmap, boolean recycle, WrapType wrapType, FilterType filterType) {
		final Bitmap texture = getBitmap(resID);
		TextureInfo tInfo = addTexture(new ByteBuffer[0], texture, texture.getWidth(), texture.getHeight(), textureType, texture.getConfig(), mipmap, recycle, wrapType, filterType);
		if(recycle && tInfo.getTextureId() > 0)
			texture.recycle();
		else 
			tInfo.setTexture(texture);
		return tInfo;
	}
	
	public TextureInfo addTexture(Bitmap texture) {
		return addTexture(texture, TextureType.DIFFUSE);
	}
	public TextureInfo addTexture(Bitmap texture, TextureType textureType) {
		return this.addTexture(texture, textureType, true, false, WrapType.REPEAT, FilterType.LINEAR);	
	}
	
	public TextureInfo addTexture(Bitmap texture, TextureType textureType, boolean mipmap) {
	    return this.addTexture(texture, textureType, mipmap, false, WrapType.REPEAT, FilterType.LINEAR);
	}

	public TextureInfo addTexture(Bitmap texture, boolean mipmap, boolean recycle) {
	    return this.addTexture(texture, TextureType.DIFFUSE, mipmap, recycle, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	public TextureInfo addTexture(Bitmap texture, TextureType textureType, WrapType wrapType, FilterType filterType) {
		return this.addTexture(texture, textureType, true, false, wrapType, filterType);	
	}

	public TextureInfo addTexture(Bitmap texture, TextureType textureType, boolean mipmap, boolean recycle) {
		return this.addTexture(texture, textureType, mipmap, recycle, WrapType.REPEAT, FilterType.LINEAR);
	}
	public TextureInfo addTexture(Bitmap texture, TextureType textureType, boolean mipmap, boolean recycle, WrapType wrapType, FilterType filterType) {
		TextureInfo tInfo = addTexture(new ByteBuffer[0], texture, texture.getWidth(), texture.getHeight(), textureType, texture.getConfig(), mipmap, recycle, wrapType, filterType);
		if(recycle && tInfo.getTextureId() > 0)
			texture.recycle();
		else 
			tInfo.setTexture(texture);
		return tInfo;
	}
	
	public TextureInfo addTexture(ByteBuffer buffer, int width, int height) {
		return addTexture(buffer, width, height, TextureType.DIFFUSE);
	}
	
	public TextureInfo addTexture(ByteBuffer buffer, int width, int height, TextureType textureType) {
		return addTexture(buffer, null, width, height, textureType, Config.ARGB_8888, false, false, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	public TextureInfo addTexture(ByteBuffer[] buffer, Bitmap texture, int width, int height, TextureType textureType, Config bitmapConfig, boolean mipmap, boolean recycle, WrapType wrapType, FilterType filterType) {
		return addTexture(buffer, texture, width, height, textureType, bitmapConfig, mipmap, recycle, false, wrapType, filterType);
	}
	
	public TextureInfo addTexture(ByteBuffer buffer, Bitmap texture, int width, int height, TextureType textureType, Config bitmapConfig, boolean mipmap, boolean recycle, WrapType wrapType, FilterType filterType) {
		return addTexture(new ByteBuffer[] { buffer }, texture, width, height, textureType, bitmapConfig, mipmap, recycle, false, wrapType, filterType);
	}
	
	public TextureInfo addTexture(TextureInfo textureInfo) {
		TextureInfo newInfo;
		GLES20.glDeleteTextures(1, new int[] { textureInfo.getTextureId() }, 0);
		TextureInfo oldInfo = new TextureInfo(textureInfo);
		
		if (textureInfo.getCompressionType() == CompressionType.NONE) {
			if(textureInfo.getTextureType() == TextureType.CUBE_MAP) {
				newInfo = addCubemapTextures(textureInfo.getTextures(), textureInfo.isMipmap(), textureInfo.shouldRecycle());
			} else if(textureInfo.getTextureType() == TextureType.FRAME_BUFFER) {
				newInfo = addTexture(null, textureInfo.getWidth(), textureInfo.getHeight(), TextureType.FRAME_BUFFER);
			} else {
				if (textureInfo.getTexture() == null) {
					newInfo = addTexture(textureInfo.getBuffer(), null, textureInfo.getWidth(), textureInfo.getHeight(), textureInfo.getTextureType(), textureInfo.getBitmapConfig(), false, false, textureInfo.getWrapType(), textureInfo.getFilterType());
				} else {
					newInfo = addTexture(textureInfo.getTexture(), textureInfo.getTextureType(), textureInfo.isMipmap(), textureInfo.shouldRecycle(), textureInfo.getWrapType(), textureInfo.getFilterType());
				}
			}
		} else {
			newInfo = addTexture(textureInfo.getBuffer(), null, textureInfo.getWidth(), textureInfo.getHeight(), textureInfo.getTextureType(), null, false, false, false, textureInfo.getWrapType(), textureInfo.getFilterType(), textureInfo.getCompressionType(), textureInfo.getInternalFormat());
		}
		
		// remove newly added texture info because we're sticking with the old one.
		mTextureInfoList.remove(newInfo);
		textureInfo.setFrom(newInfo);
		textureInfo.setTextureName(oldInfo.getTextureName());

		return textureInfo;
	}
	
	public TextureInfo addTexture(ByteBuffer[] buffer, Bitmap texture, int width, int height, TextureType textureType, Config bitmapConfig, boolean mipmap, boolean recycle, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		return addTexture(buffer, texture, width, height, textureType, bitmapConfig, mipmap, recycle, isExistingTexture, wrapType, filterType, CompressionType.NONE, 0);
	}
	
	public TextureInfo addTexture(ByteBuffer buffer, Bitmap texture, int width, int height, TextureType textureType, Config bitmapConfig, boolean mipmap, boolean recycle, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		return addTexture(new ByteBuffer[] { buffer }, texture, width, height, textureType, bitmapConfig, mipmap, recycle, isExistingTexture, wrapType, filterType, CompressionType.NONE, 0);
	}
	
	public TextureInfo addTexture(ByteBuffer[] buffer, Bitmap texture, int width, int height, TextureType textureType, Config bitmapConfig, boolean mipmap, boolean recycle, boolean isExistingTexture, WrapType wrapType, FilterType filterType, CompressionType compressionType, int compressionFormat) {
		int bitmapFormat = bitmapConfig == Config.ARGB_8888 ? GLES20.GL_RGBA : GLES20.GL_RGB;
		
		int[] textures = new int[1];
		GLES20.glGenTextures(1, textures, 0);
		int textureId = textures[0];
		if(textureId > 0) {
			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);        
	        
			if((mipmap && compressionType == CompressionType.NONE) || 
					// Manual mipmapped textures are included
					(buffer.length > 1)){
				if(filterType==FilterType.LINEAR)
					GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);
				else
					GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST_MIPMAP_NEAREST);				
			}else{
				if(filterType==FilterType.LINEAR)
			       	GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
				else
					GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
			}
	
			if(filterType==FilterType.LINEAR)
				GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
			else
				GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST);
			
			if(wrapType==WrapType.REPEAT){
				GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
	        	GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
			}else{
				GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
	        	GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
			}
	        	
	        if(texture == null) {
	        	if (compressionType == CompressionType.NONE) {
	        		if ((buffer != null && buffer.length == 0) || buffer == null) {
	        			GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, bitmapFormat, width, height, 0, bitmapFormat, GLES20.GL_UNSIGNED_BYTE, null);
	        		} else {
	        			int w = width, h = height;
		        		for (int i = 0; i < buffer.length; i++) {
		        			GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, i, bitmapFormat, w, h, 0, bitmapFormat, GLES20.GL_UNSIGNED_BYTE, buffer[i]);
		        			w = w > 1 ? w / 2 : 1;
		        			h = h > 1 ? h / 2 : 1;
		        		}
	        		}
	        	} else { 
	        		if ((buffer != null && buffer.length == 0) || buffer == null) {
	        			GLES20.glCompressedTexImage2D(GLES20.GL_TEXTURE_2D, 0, compressionFormat, width, height, 0, 0, null);
	        		} else {
	        			int w = width, h = height;
		        		for (int i = 0; i < buffer.length; i++) {
	        				GLES20.glCompressedTexImage2D(GLES20.GL_TEXTURE_2D, i, compressionFormat, w, h, 0, buffer[i].capacity(), buffer[i]);
	        				w = w > 1 ? w / 2 : 1;
	        				h = h > 1 ? h / 2 : 1;
		        		}
	        		}
	        	}
	        } else
	        	GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmapFormat, texture, 0);
	
	        if(mipmap && compressionType == CompressionType.NONE)
	        	GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
		} else {
			mShouldValidateTextures = true;
		}
        
        TextureInfo textureInfo = mCurrentValidatingTexInfo == null ?  new TextureInfo(textureId, textureType) : mCurrentValidatingTexInfo;
        if(mCurrentValidatingTexInfo == null) {
	        textureInfo.setWidth(width);
	        textureInfo.setHeight(height);
	        textureInfo.setBitmapConfig(bitmapConfig);
	        textureInfo.setMipmap(mipmap);
	        textureInfo.setWrapType(wrapType);
	        textureInfo.setFilterType(filterType);
	        textureInfo.shouldRecycle(recycle);
	        textureInfo.setCompressionType(compressionType);
	        textureInfo.setBuffer(buffer);
	        if(compressionType != CompressionType.NONE){
	        	textureInfo.setInternalFormat(compressionFormat);
	        }
	        
	        if(!recycle && compressionType == CompressionType.NONE)
	        	textureInfo.setTexture(texture);
        } else {
        	textureInfo.setTextureId(textureId);
        }
        for (int i = 0; i < buffer.length; i++) {
        	if(buffer[i] != null) {
        		buffer[i].limit(0);
        	}
        }
        buffer = null;
        if(!isExistingTexture && mCurrentValidatingTexInfo == null)
        	mTextureInfoList.add(textureInfo);
        
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);  
        
        return textureInfo;
	}
	
	/**
	 * Dynamically generates an ETC1 texture on-the-fly from given resource ID and automatically binds the newly
	 * generated texture.
	 * 
	 * @param resID
	 * @param textureType
	 * @return
	 */
	public TextureInfo addEtc1Texture(int resID, TextureType textureType) {
		return addEtc1Texture(getBitmap(resID), textureType);
	}
	
	/**
	 * Dynamically generates an ETC1 texture on-the-fly from given bitmap and automatically binds the newly generated
	 * texture.
	 * 
	 * @param bitmap
	 * @param textureType
	 * @return
	 */
	public TextureInfo addEtc1Texture(Bitmap bitmap, TextureType textureType) {
		int imageSize = bitmap.getRowBytes() * bitmap.getHeight();
		ByteBuffer uncompressedBuffer = ByteBuffer.allocateDirect(imageSize);
		bitmap.copyPixelsToBuffer(uncompressedBuffer);
		uncompressedBuffer.position(0);
		
		ByteBuffer compressedBuffer = ByteBuffer.allocateDirect(ETC1.getEncodedDataSize(bitmap.getWidth(), bitmap.getHeight())).order(ByteOrder.nativeOrder());
		ETC1.encodeImage(uncompressedBuffer, bitmap.getWidth(), bitmap.getHeight(), 2, 2 * bitmap.getWidth(), compressedBuffer);
		
		return addEtc1Texture(compressedBuffer, bitmap.getWidth(), bitmap.getHeight(), textureType);
	}
	
	/**
	 * Attempts to add ETC1 compressed texture and fall back to uncompressed bitmap upon failure.
	 * @param compressedTex InputStream of the PKM texture resource
	 * @param fallbackTex Uncompressed Bitmap resource to fallback on if adding ETC1 texture fails
	 * @return TextureInfo
	 */
	public TextureInfo addEtc1Texture(InputStream compressedTex, Bitmap fallbackTex) {
		return addEtc1Texture(compressedTex, fallbackTex, TextureType.DIFFUSE);
	}
	
	public TextureInfo addEtc1Texture(InputStream compressedTex, Bitmap fallbackTex, TextureType textureType) {
		ETC1Util.ETC1Texture texture = null;
		TextureInfo textureInfo = null;
		try {
			texture = ETC1Util.createTexture(compressedTex);
		} catch (IOException e) {
			Log.e("addEtc1Texture", e.getMessage());
		} finally {
			if (texture == null) {
				textureInfo = addTexture(fallbackTex);
				Log.d("ETC1", "Falling back to uncompressed texture");
			} else {
				textureInfo = addEtc1Texture(texture.getData(), texture.getWidth(), texture.getHeight(), textureType);
				Log.d("ETC1", "ETC1 texture load successful");
			}
		}
		
		return textureInfo;
	}
	
	/**
	 * Add mipmap-chained ETC1 texture. 
	 * @param context
	 * @param resourceIds
	 * @return
	 */
	public TextureInfo addEtc1Texture(int[] resourceIds, TextureType textureType, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		if (resourceIds == null) return null;
		ByteBuffer[] mipmapChain = new ByteBuffer[resourceIds.length];
		int mip_0_width = 1, mip_0_height = 1;
		try {
			for (int i = 0, length = resourceIds.length; i < length; i++) {
				ETC1Util.ETC1Texture texture = ETC1Util.createTexture(mContext.getResources().openRawResource(resourceIds[i]));
				mipmapChain[i] = texture.getData();
				if (i == 0) {
					mip_0_width = texture.getWidth();
					mip_0_height = texture.getHeight();
				}
			}
		} catch (IOException e) {
			RajLog.e(e.getMessage());
			e.printStackTrace();
		}
		
		return addTexture(mipmapChain, null, mip_0_width, mip_0_height, textureType, null, true, false, isExistingTexture, wrapType, filterType, CompressionType.ETC1, ETC1.ETC1_RGB8_OES);
	}
	
	/**
	 * Add mipmap-chained ETC1 texture. 
	 * @param context
	 * @param resourceIds
	 * @return
	 */
	public TextureInfo addEtc1Texture(int[] resourceIds, TextureType textureType) {
		return addEtc1Texture(resourceIds, textureType, false, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	public TextureInfo addEtc1Texture(ByteBuffer buffer, int width, int height) {
		return addEtc1Texture(buffer, width, height, TextureType.DIFFUSE);
	}

	/**
	 * Adds and binds ETC1 compressed texture. Due to limitations with compressed textures, 
	 * automatic mipmap generation is disabled.
	 * 
	 * All devices with OpenGL ES 2.0 API should be able to handle this type of compression. 
	 */
	public TextureInfo addEtc1Texture(ByteBuffer buffer, int width, int height, TextureType textureType) {
		return addEtc1Texture(buffer, width, height, textureType, false, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	public TextureInfo addEtc1Texture(ByteBuffer buffer, int width, int height, TextureType textureType, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		return addTexture(new ByteBuffer[]{buffer}, null, width, height, textureType, null, false, false, isExistingTexture, wrapType, filterType, CompressionType.ETC1, ETC1.ETC1_RGB8_OES);
	}
	
	public TextureInfo addEtc1Texture(ByteBuffer[] buffer, int width, int height, TextureType textureType, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		return addTexture(buffer, null, width, height, textureType, null, false, false, isExistingTexture, wrapType, filterType, CompressionType.ETC1, ETC1.ETC1_RGB8_OES);
	}
	
	public TextureInfo addPalettedTexture(ByteBuffer buffer, int width, int height, TextureType textureType, PaletteFormat format) {
		return addPalettedTexture(buffer, width, height, textureType, format, false, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	/**
	 * Adds and binds paletted texture. Pass in multiple buffer corresponding to different mipmap levels.
	 */
	public TextureInfo addPalettedTexture(ByteBuffer[] buffer, int width, int height, TextureType textureType, PaletteFormat format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		int internalformat;
		switch (format) {
			case PALETTE4_RGB8:
				internalformat = GL_PALETTE4_RGB8_OES;
				break;
			case PALETTE4_RGBA8:
				internalformat = GL_PALETTE4_RGBA8_OES;
				break;
			case PALETTE4_R5_G6_B5:
				internalformat = GL_PALETTE4_R5_G6_B5_OES;
				break;
			case PALETTE4_RGBA4:
				internalformat = GL_PALETTE4_RGBA4_OES;
				break;
			case PALETTE4_RGB5_A1:
				internalformat = GL_PALETTE4_RGB5_A1_OES;
				break;
			case PALETTE8_RGB8:
				internalformat = GL_PALETTE8_RGB8_OES;
				break;
			case PALETTE8_RGBA8:
			default:
				internalformat = GL_PALETTE8_RGBA8_OES;
				break;
			case PALETTE8_R5_G6_B5:
				internalformat = GL_PALETTE8_R5_G6_B5_OES;
				break;
			case PALETTE8_RGBA4:
				internalformat = GL_PALETTE8_RGBA4_OES;
				break;
			case PALETTE8_RGB5_A1:
				internalformat = GL_PALETTE8_RGB5_A1_OES;
				break;
		}
		
		return addTexture(buffer, null, width, height, textureType, null, false, false, isExistingTexture, wrapType, filterType, CompressionType.PALETTED, internalformat);
	}
	
	public TextureInfo addPalettedTexture(ByteBuffer buffer, int width, int height, TextureType textureType, PaletteFormat format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		return addPalettedTexture(new ByteBuffer[]{buffer}, width, height, textureType, format, isExistingTexture, wrapType, filterType);
	}
	
	public TextureInfo add3dcTexture(ByteBuffer buffer, int width, int height, TextureType textureType, ThreeDcFormat format) {
		return add3dcTexture(buffer, width, height, textureType, format, false, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	/**
	 * Adds and binds ATI 3Dc compressed texture. This compression type is most used for
	 * compressing normal map textures.
	 */
	public TextureInfo add3dcTexture(ByteBuffer[] buffer, int width, int height, TextureType textureType, ThreeDcFormat format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		if (format == ThreeDcFormat.X) 
			return addTexture(buffer, null, width, height, textureType, null, false, false, isExistingTexture, wrapType, filterType, CompressionType.THREEDC, GLES11Ext.GL_3DC_X_AMD);
		else
			return addTexture(buffer, null, width, height, textureType, null, false, false, isExistingTexture, wrapType, filterType, CompressionType.THREEDC, GLES11Ext.GL_3DC_XY_AMD);
	}
	
	public TextureInfo add3dcTexture(ByteBuffer buffer, int width, int height, TextureType textureType, ThreeDcFormat format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		return add3dcTexture(new ByteBuffer[] { buffer }, width, height, textureType, format, isExistingTexture, wrapType, filterType);
	}
	
	public TextureInfo addAtcTexture(ByteBuffer buffer, int width, int height, TextureType textureType, AtcFormat format) {
		return addAtcTexture(buffer, width, height, textureType, format, false, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	/**
	 * Adds and binds AMD compressed texture. To use mipmaps, pass in more than one items in the buffer, 
	 * starting with mipmap level 0.
	 * 
	 * This method will only work on devices that support AMD texture compression. Most Adreno GPU
	 * based devices such as Nexus One and HTC Desire support AMD texture compression.
	 */
	public TextureInfo addAtcTexture(ByteBuffer[] buffer, int width, int height, TextureType textureType, AtcFormat format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		int internalformat;
		switch(format) {
			case RGB:
				internalformat = GLES11Ext.GL_ATC_RGB_AMD;
				break;
			case RGBA_EXPLICIT:
			default:
				internalformat = GLES11Ext.GL_ATC_RGBA_EXPLICIT_ALPHA_AMD;
				break;
			case RGBA_INTERPOLATED:
				internalformat = GLES11Ext.GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD;
				break;
		}
		return addTexture(buffer, null, width, height, textureType, null, false, false, isExistingTexture, wrapType, filterType, CompressionType.ATC, internalformat);
	}
	
	public TextureInfo addAtcTexture(ByteBuffer buffer, int width, int height, TextureType textureType, AtcFormat format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		return addAtcTexture(new ByteBuffer[]{buffer}, width, height, textureType, format, isExistingTexture, wrapType, filterType);
	}
	
	public TextureInfo addDxt1Texture(ByteBuffer buffer, int width, int height, TextureType textureType, Dxt1Format format) {
		return addDxt1Texture(buffer, width, height, textureType, format, false, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	/**
	 * Adds and binds DXT1 variant of S3 compressed texture.
	 * 
	 * This method will only work in devices that support S3 texture compression. Most Nvidia Tegra2 GPU
	 * based devices such as Motorola Xoom, and Atrix support S3 texture comrpession. 
	 */
	public TextureInfo addDxt1Texture(ByteBuffer[] buffer, int width, int height, TextureType textureType, Dxt1Format format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		int internalformat;
		switch (format) {
			case RGB:
				internalformat = GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
				break;
			case RGBA:
			default:
				internalformat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
				break;
		}
		return addTexture(buffer, null, width, height, textureType, null, false, false, isExistingTexture, wrapType, filterType, CompressionType.DXT1, internalformat);
	}
	
	public TextureInfo addDxt1Texture(ByteBuffer buffer, int width, int height, TextureType textureType, Dxt1Format format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		return addDxt1Texture(new ByteBuffer[]{buffer}, width, height, textureType, format, isExistingTexture, wrapType, filterType);
	}
	
	public TextureInfo addPvrtcTexture(ByteBuffer buffer, int width, int height, TextureType textureType, PvrtcFormat format) {
		return addPvrtcTexture(buffer, width, height, textureType, format, false, WrapType.REPEAT, FilterType.LINEAR);
	}
	
	/**
	 * Adds and binds PowerVR compressed texture. Due to limitations with compressed textures, 
	 * automatic mipmap generation is disabled.
	 * 
	 * This method will only work on devices that support PowerVR texture compression. Most PowerVR GPU
	 * based devices such as Nexus S and Galaxy S3 support AMD texture compression.
	 */
	public TextureInfo addPvrtcTexture(ByteBuffer[] buffer, int width, int height, TextureType textureType, PvrtcFormat format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		int internalformat;
		switch (format) {
			case RGB_2BPP:
				internalformat = GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
				break;
			case RGB_4BPP:
				internalformat = GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
				break;
			case RGBA_2BPP:
				internalformat = GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
				break;
			case RGBA_4BPP:
			default:
				internalformat = GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
				break;
		}
		return addTexture(buffer, null, width, height, textureType, null, false, false, isExistingTexture, wrapType, filterType, CompressionType.PVRTC, internalformat);
	}
	
	public TextureInfo addPvrtcTexture(ByteBuffer buffer, int width, int height, TextureType textureType, PvrtcFormat format, boolean isExistingTexture, WrapType wrapType, FilterType filterType) {
		return addPvrtcTexture(new ByteBuffer[]{buffer}, width, height, textureType, format, isExistingTexture, wrapType, filterType);
	}
	
	/**
	 * This only works for API Level 15 and higher.
	 * Thanks to Lubomir Panak (@drakh)
	 * <p>
	 * How to use:
	 * <pre><code>
	 * protected void initScene() {
	 * 		super.initScene();
	 * 		mLight = new DirectionalLight(0, 0, 1);
	 * 		mCamera.setPosition(0, 0, -17);
	 * 		
	 * 		VideoMaterial material = new VideoMaterial();
	 * 		TextureInfo tInfo = mTextureManager.addVideoTexture();
	 * 		
	 * 		mTexture = new SurfaceTexture(tInfo.getTextureId());
	 * 		
	 * 		mMediaPlayer = MediaPlayer.create(getContext(), R.raw.nemo);
	 * 		mMediaPlayer.setSurface(new Surface(mTexture));
	 * 		mMediaPlayer.start();
	 * 		
	 * 		BaseObject3D cube = new Plane(2, 2, 1, 1);
	 * 		cube.setMaterial(material);
	 * 		cube.addTexture(tInfo);
	 * 		cube.addLight(mLight);
	 * 		addChild(cube);
	 * 	}
	 * 
	 * 	public void onDrawFrame(GL10 glUnused) {
	 * 		mTexture.updateTexImage();
	 * 		super.onDrawFrame(glUnused);
	 * 	}
	 * </code></pre>
	 * @return
	 */
	public TextureInfo addVideoTexture() {
		TextureType textureType = TextureType.VIDEO_TEXTURE;
		int[] textures = new int[1];
		GLES20.glGenTextures(1, textures, 0);
		int textureId = textures[0];
		GLES20.glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId);
		GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
				GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
		GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
				GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
		GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
				GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
		GLES20.glTexParameterf(GL_TEXTURE_EXTERNAL_OES,
				GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
		TextureInfo textureInfo = new TextureInfo(textureId, textureType);
		return textureInfo;
	}
	
	public TextureInfo addCubemapTextures(int[] textures) {
		return addCubemapTextures(textures, false);
	}
	public TextureInfo addCubemapTextures(int[] textures, boolean mipmap) {
		return addCubemapTextures(textures, false, false);
	}
	public TextureInfo addCubemapTextures(int[] textures, boolean mipmap, boolean recycle) {
		return addCubemapTextures(textures, mipmap, recycle, false);
	}
	public TextureInfo addCubemapTextures(int[] textures, boolean mipmap, boolean recycle, boolean isExistingTexture) {
		final Bitmap[] bitmapTextures = new Bitmap[textures.length];
		for (int i=0;i<textures.length;i++)
			bitmapTextures[i] = getBitmap(textures[i]);
		
		return addCubemapTextures(bitmapTextures, mipmap, recycle, isExistingTexture);
	}
	
	public TextureInfo addCubemapTextures(Bitmap[] textures) {
		return addCubemapTextures(textures, false);
	}
	public TextureInfo addCubemapTextures(Bitmap[] textures, boolean mipmap) {
		return addCubemapTextures(textures, false, false);
	}
	public TextureInfo addCubemapTextures(Bitmap[] textures, boolean mipmap, boolean recycle) {
		return addCubemapTextures(textures, mipmap, recycle, false);
	}
	public TextureInfo addCubemapTextures(Bitmap[] textures, boolean mipmap, boolean recycle, boolean isExistingTexture) {
		int[] textureIds = new int[1];
		
		GLES20.glGenTextures(1, textureIds, 0);
		int textureId = textureIds[0];
		
		TextureInfo textureInfo = mCurrentValidatingTexInfo == null ?  new TextureInfo(textureId) : mCurrentValidatingTexInfo;
		if(!isExistingTexture && mCurrentValidatingTexInfo == null) mTextureInfoList.add(textureInfo);
		
		if(mCurrentValidatingTexInfo == null) {
			textureInfo.setWidth(textures[0].getWidth());
			textureInfo.setHeight(textures[0].getHeight());
			textureInfo.setTextureType(TextureType.CUBE_MAP);
			textureInfo.setBitmapConfig(textures[0].getConfig());
			textureInfo.setCompressionType(CompressionType.NONE);
			textureInfo.setMipmap(mipmap);
			textureInfo.shouldRecycle(recycle);			
			textureInfo.setIsCubeMap(true);
		} else {
			textureInfo.setTextureId(textureId);
		}
		
		if(textureId > 0) {
			GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, textureId);
			if(mipmap)
				GLES20.glTexParameterf(GLES20.GL_TEXTURE_CUBE_MAP, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);
			else 
				GLES20.glTexParameterf(GLES20.GL_TEXTURE_CUBE_MAP, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
	        GLES20.glTexParameterf(GLES20.GL_TEXTURE_CUBE_MAP, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
	        GLES20.glTexParameteri(GLES20.GL_TEXTURE_CUBE_MAP, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
	        GLES20.glTexParameteri(GLES20.GL_TEXTURE_CUBE_MAP, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
	        
	        for(int i=0; i<6; i++) {
	        	GLES20.glHint(GLES20.GL_GENERATE_MIPMAP_HINT, GLES20.GL_NICEST);
	        	GLUtils.texImage2D(CUBE_FACES[i], 0, textures[i], 0);
	        	if(recycle && textureId > 0) textures[i].recycle();
	        }
	        
	        if(mipmap) GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_CUBE_MAP);
		} else {
			mShouldValidateTextures = true;
		}
        
        if(!recycle)
        	textureInfo.setTextures(textures);
        
        GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, textureId);
		return textureInfo;
	}
	
	public void updateCubemapTextures(TextureInfo textureInfo, Bitmap[] textures)
	{
		textureInfo.setTextures(textures);
		mShouldUpdateTextures = true;
		mTexturesToUpdate.add(textureInfo);
	}
	
	public void updateCubemapTextures(TextureInfo textureInfo)
	{
		Bitmap[] textures = textureInfo.getTextures();
		GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, textureInfo.getTextureId());
		
		for(int i=0; i<6; i++) {
			int bitmapFormat = textures[i].getConfig() == Config.ARGB_8888 ? GLES20.GL_RGBA : GLES20.GL_RGB;
			GLUtils.texSubImage2D(CUBE_FACES[i], 0, 0, 0, textures[i], bitmapFormat, GLES20.GL_UNSIGNED_BYTE);
		}
		if(textureInfo.isMipmap())
        	GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_CUBE_MAP);
		
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
	}
	
	public int getNumTextures() {
		return mTextureInfoList.size();
	}
	
	/**
	 * Please use updateTexture(TextureInfo textureInfo, Bitmap texture)
	 * @deprecated
	 * @param textureId
	 * @param texture
	 */
	@Deprecated
	public void updateTexture(Integer textureId, Bitmap texture) {
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId.intValue());
		GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, texture);
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
	}
	
	public void updateTexture(TextureInfo textureInfo, Bitmap texture) {
		textureInfo.setTexture(texture);
		mShouldUpdateTextures = true;
		mTexturesToUpdate.add(textureInfo);
	}
	
	public void updateTexture(TextureInfo textureInfo) {
		Bitmap texture = textureInfo.getTexture();
		int bitmapFormat = texture.getConfig() == Config.ARGB_8888 ? GLES20.GL_RGBA : GLES20.GL_RGB;
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureInfo.getTextureId());
		GLUtils.texSubImage2D(GLES20.GL_TEXTURE_2D, 0, 0, 0, texture, bitmapFormat, GLES20.GL_UNSIGNED_BYTE);
        if(textureInfo.isMipmap())
        	GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
		
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
	}

	public void reload() {
		TextureInfo tInfo = null;
		
		int len = getNumTextures(); 
		
		for(int i=0; i<len; i++) {
			tInfo = mTextureInfoList.get(i);
			tInfo.setFrom(addTexture(tInfo));
		}
	}
	
	public void reset() {
		int count = mTextureInfoList.size();
		int[] textures = new int[count];
		for(int i=0; i<count; i++)
		{
			TextureInfo ti = mTextureInfoList.get(i);
			if(ti.getTexture() != null) ti.getTexture().recycle();
			textures[i] = ti.getTextureId();
		}

		GLES20.glDeleteTextures(count, textures, 0);
		
		mTextureInfoList.clear();
	}
	
	public void removeTexture(TextureInfo textureInfo) {
		mTextureInfoList.remove(textureInfo);
		GLES20.glDeleteTextures(1, new int[]{textureInfo.getTextureId()}, 0);
	}
	
	public void removeTextures(ArrayList<TextureInfo> textureInfoList) {
		try {
			int count = textureInfoList.size();
			int[] textures = new int[count];
			int i;
			for (i = 0; i < count; ++i) {
				Integer textureId = textureInfoList.get(i).getTextureId();
				textures[i] = textureId.intValue();
				mTextureInfoList.remove(textureInfoList.get(i));
			}
			textureInfoList.clear();
			GLES20.glDeleteTextures(count, textures, 0);
		} catch (Exception e) {
			Log.e("RajawaliTextureManager", e.getMessage());
		}
	}
	
	public List<TextureInfo> getTextureInfoList() {
		return mTextureInfoList;
	}
	
	public void validateTextures() {
		if(mShouldValidateTextures) {
			// Iterating a temporary list is better for the memory
			// consumption than using iterators.
			List<TextureInfo> tempList = new ArrayList<TextureInfo>();
			tempList.addAll(mTextureInfoList);

		    for (int i = 0; i < tempList.size(); ++i) {
		    	TextureInfo inf = tempList.get(i);
		    	if(inf.getTextureId() == 0) {
		    		mCurrentValidatingTexInfo = inf;
					addTexture(inf);
					mCurrentValidatingTexInfo = null;
		    	}
		    }
			mShouldValidateTextures = false;
		}
		if(mShouldUpdateTextures) {
			while(!mTexturesToUpdate.isEmpty())
			{
				TextureInfo tInfo = mTexturesToUpdate.pop();
				if(tInfo.isCubeMap())
					updateCubemapTextures(tInfo);
				else
					updateTexture(tInfo);
			}
			mShouldUpdateTextures = false;
		}
	}
	
	/**
	 * Retrieve a bitmap for the given resource.
	 * 
	 * @param resID
	 * @return
	 */
	private final Bitmap getBitmap(int resID) {
		return BitmapFactory.decodeResource(mContext.getResources(), resID);
	}
	
} public class Plane extends BaseObject3D {

	protected float mWidth;
	protected float mHeight;
	protected int mSegmentsW;
	protected int mSegmentsH;

	public Plane() {
		this(1f, 1f, 1, 1);
	}

	public Plane(float width, float height, int segmentsW, int segmentsH) {
		super();
		mWidth = width;
		mHeight = height;
		mSegmentsW = segmentsW;
		mSegmentsH = segmentsH;
		init();
	}

	private void init() {
		int i, j;
		int numVertices = (mSegmentsW + 1) * (mSegmentsH + 1);
		float[] vertices = new float[numVertices * 3];
		float[] textureCoords = new float[numVertices * 2];
		float[] normals = new float[numVertices * 3];
		float[] colors = new float[numVertices * 4];
		int[] indices = new int[mSegmentsW * mSegmentsH * 6];
		int vertexCount = 0;
		int texCoordCount = 0;

		for (i = 0; i <= mSegmentsW; i++) {
			for (j = 0; j <= mSegmentsH; j++) {
				vertices[vertexCount] = ((float) i / (float) mSegmentsW - 0.5f) * mWidth;
				vertices[vertexCount + 1] = ((float) j / (float) mSegmentsH - 0.5f) * mHeight;
				vertices[vertexCount + 2] = 0;

				textureCoords[texCoordCount++] = (float) i / (float) mSegmentsW;
				textureCoords[texCoordCount++] = 1.0f - (float) j / (float) mSegmentsH;

				normals[vertexCount] = 0;
				normals[vertexCount + 1] = 0;
				normals[vertexCount + 2] = 1;

				vertexCount += 3;
			}
		}

		int colspan = mSegmentsH + 1;
		int indexCount = 0;

		for (int col = 0; col < mSegmentsW; col++) {
			for (int row = 0; row < mSegmentsH; row++) {
				int ul = col * colspan + row;
				int ll = ul + 1;
				int ur = (col + 1) * colspan + row;
				int lr = ur + 1;

				indices[indexCount++] = (int) ul;
				indices[indexCount++] = (int) ur;
				indices[indexCount++] = (int) lr;

				indices[indexCount++] = (int) ul;
				indices[indexCount++] = (int) lr;
				indices[indexCount++] = (int) ll;
			}
		}

		int numColors = numVertices * 4;
		for (j = 0; j < numColors; j += 4)
		{
			colors[j] = 1.0f;
			colors[j + 1] = 1.0f;
			colors[j + 2] = 1.0f;
			colors[j + 3] = 1.0f;
		}

		setData(vertices, normals, textureCoords, colors, indices);
	}
} public class RajawaliRenderer implements GLSurfaceView.Renderer, INode {
	protected final int GL_COVERAGE_BUFFER_BIT_NV = 0x8000;

	protected Context mContext;

	protected float mEyeZ = 4.0f;
	protected float mFrameRate;
	protected double mLastMeasuredFPS;
	protected FPSUpdateListener mFPSUpdateListener;

	protected SharedPreferences preferences;

	protected int mViewportWidth, mViewportHeight;
	protected WallpaperService.Engine mWallpaperEngine;
	protected GLSurfaceView mSurfaceView;
	protected Timer mTimer;
	protected int mFrameCount;
	private long mStartTime = System.nanoTime();

	protected float[] mVMatrix = new float[16];
	protected float[] mPMatrix = new float[16];
	protected List<BaseObject3D> mChildren;
	protected boolean mEnableDepthBuffer = true;

	protected TextureManager mTextureManager;
	protected PostProcessingRenderer mPostProcessingRenderer;

	/**
	 * Deprecated. Use setSceneCachingEnabled(false) instead.
	 */
	@Deprecated
	protected boolean mClearChildren = true;

	/**
	* The camera currently in use.
	* Not thread safe for speed, should
	* only be used by GL thread (onDrawFrame() and render())
	* or prior to rendering such as initScene(). 
	*/
	protected Camera mCamera;
	/**
	* List of all cameras in the scene.
	*/
	protected List<Camera> mCameras; 
	/**
	* Temporary camera which will be switched to by the GL thread.
	* Guarded by mNextCameraLock
	*/
	protected Camera mNextCamera;
	private final Object mNextCameraLock = new Object();

	protected float mRed, mBlue, mGreen, mAlpha;
	protected Cube mSkybox;
	protected TextureInfo mSkyboxTextureInfo;
	protected static int mMaxLights = 1;

	protected ColorPickerInfo mPickerInfo;

	protected List<IPostProcessingFilter> mFilters;
	protected boolean mReloadPickerInfo;
	protected static boolean mFogEnabled;
	protected boolean mUsesCoverageAa;

	public static boolean supportsUIntBuffers = false;

	protected boolean mSceneInitialized;
	/**
	 * Scene caching stores all textures and relevant OpenGL-specific
	 * data. This is used when the OpenGL context needs to be restored.
	 * The context typically needs to be restored when the application
	 * is re-activated or when a live wallpaper is rotated. 
	 */
	private boolean mSceneCachingEnabled;

	protected List<IRendererPlugin> mPlugins;

	public RajawaliRenderer(Context context) {
		RajLog.i("IMPORTANT: Rajawali's coordinate system has changed. It now reflects");
		RajLog.i("the OpenGL standard. Please invert the camera's z coordinate or");
		RajLog.i("call mCamera.setLookAt(0, 0, 0).");
		
		AMaterial.setLoaderContext(context);
		
		mContext = context;
		mChildren = Collections.synchronizedList(new CopyOnWriteArrayList<BaseObject3D>());
		mFilters = Collections.synchronizedList(new CopyOnWriteArrayList<IPostProcessingFilter>());
		mPlugins = Collections.synchronizedList(new CopyOnWriteArrayList<IRendererPlugin>());
		mCamera = new Camera();
		mCameras = Collections.synchronizedList(new CopyOnWriteArrayList<Camera>());
		addCamera(mCamera);
		mCamera.setZ(mEyeZ);
		mAlpha = 0;
		mSceneCachingEnabled = true;
		mPostProcessingRenderer = new PostProcessingRenderer(this);
		mFrameRate = getRefreshRate();
	}

	/**
	* Sets the camera currently being used to display the scene.
	* 
	* @param mCamera Camera object to display the scene with.
	*/
	public void setCamera(Camera camera) {
		synchronized (mNextCameraLock) {
			mNextCamera = camera;
		}
	}
	  
	/**
	* Sets the camera currently being used to display the scene.
	* 
	* @param camera Index of the camera to use.
	*/
	public void setCamera(int camera) {
		setCamera(mCameras.get(camera));
	}

	/**
	* Fetches the camera currently being used to display the scene.
	* Note that the camera is not thread safe so this should be used
	* with extreme caution.
	* 
	* @return Camera object currently used for the scene.
	* @see {@link RajawaliRenderer#mCamera}
	*/
	public Camera getCamera() {
		return this.mCamera;
	}
	
	/**
	* Fetches the specified camera. 
	* 
	* @param camera Index of the camera to fetch.
	* @return Camera which was retrieved.
	*/
	public Camera getCamera(int camera) {
		return mCameras.get(camera);
	}
	
	/**
	* Adds a camera to the renderer.
	* 
	* @param camera Camera object to add.
	* @return int The index the new camera was added at.
	*/
	public int addCamera(Camera camera) {
		mCameras.add(camera);
		return (mCameras.size() - 1);
	}
	  
	/**
	* Replaces a camera in the renderer at the specified location
	* in the list. This does not validate the index, so if it is not
	* contained in the list already, an exception will be thrown.
	* 
	* @param camera Camera object to add.
	* @param location Integer index of the camera to replace.
	*/
	public void replaceCamera(Camera camera, int location) {
		mCameras.set(location, camera);
	}
	  
	/**
	* Adds a camera with the option to switch to it immediately
	* 
	* @param camera The Camera to add.
	* @param useNow Boolean indicating if we should switch to this
	* camera immediately.
	* @return int The index the new camera was added at.
	*/
	public int addCamera(Camera camera, boolean useNow) {
		int index = addCamera(camera);
		if (useNow) setCamera(camera);
		return index;
	}
	  
	/**
	* Replaces a camera at the specified index with an option to switch to it
	* immediately.
	* 
	* @param camera The Camera to add.
	* @param location The index of the camera to replace.
	* @param useNow Boolean indicating if we should switch to this
	* camera immediately.
	*/
	public void replaceCamera(Camera camera, int location, boolean useNow) {
		replaceCamera(camera, location);
		if (useNow) setCamera(camera);
	}

	public void requestColorPickingTexture(ColorPickerInfo pickerInfo) {
		mPickerInfo = pickerInfo;
	}

	public void onDrawFrame(GL10 glUnused) {
		synchronized (mNextCameraLock) { 
			//Check if we need to switch the camera, and if so, do it.
			if (mNextCamera != null) {
				mCamera = mNextCamera;
				mNextCamera = null;
				mCamera.setProjectionMatrix(mViewportWidth, mViewportHeight);
			}
		}
		render();
		++mFrameCount;
		if (mFrameCount % 50 == 0) {
			long now = System.nanoTime();
			double elapsedS = (now - mStartTime) / 1.0e9;
			double msPerFrame = (1000 * elapsedS / mFrameCount);
			mLastMeasuredFPS = 1000 / msPerFrame;
			//RajLog.d("ms / frame: " + msPerFrame + " - fps: " + mLastMeasuredFPS);

			mFrameCount = 0;
			mStartTime = now;

			if(mFPSUpdateListener != null)
				mFPSUpdateListener.onFPSUpdate(mLastMeasuredFPS);
		}
	}

	private void render() {
		int clearMask = GLES20.GL_COLOR_BUFFER_BIT;

		ColorPickerInfo pickerInfo = mPickerInfo;
		mTextureManager.validateTextures();

		if (pickerInfo != null) {
			if(mReloadPickerInfo) pickerInfo.getPicker().reload();
			mReloadPickerInfo = false;
			pickerInfo.getPicker().bindFrameBuffer();
			GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
		} else {
			if (mFilters.size() == 0)
				GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
			else {
				if (mPostProcessingRenderer.isEnabled())
					mPostProcessingRenderer.bind();
			}

			GLES20.glClearColor(mRed, mGreen, mBlue, mAlpha);
		}

		if (mEnableDepthBuffer) {
			clearMask |= GLES20.GL_DEPTH_BUFFER_BIT;
			GLES20.glEnable(GLES20.GL_DEPTH_TEST);
			GLES20.glDepthFunc(GLES20.GL_LESS);
			GLES20.glDepthMask(true);
			GLES20.glClearDepthf(1.0f);
		}
		if (mUsesCoverageAa) {
			clearMask |= GL_COVERAGE_BUFFER_BIT_NV;
		}

		GLES20.glClear(clearMask);

		mVMatrix = mCamera.getViewMatrix();
		mPMatrix = mCamera.getProjectionMatrix();

		if (mSkybox != null) {
			GLES20.glDisable(GLES20.GL_DEPTH_TEST);
			GLES20.glDepthMask(false);

			mSkybox.setPosition(mCamera.getX(), mCamera.getY(), mCamera.getZ());
			mSkybox.render(mCamera, mPMatrix, mVMatrix, pickerInfo);

			if (mEnableDepthBuffer) {
				GLES20.glEnable(GLES20.GL_DEPTH_TEST);
				GLES20.glDepthMask(true);
			}
		}

		mCamera.updateFrustum(mPMatrix,mVMatrix); //update frustum plane

		for (int i = 0; i < mChildren.size(); i++)
			mChildren.get(i).render(mCamera, mPMatrix, mVMatrix, pickerInfo);

		if (pickerInfo != null) {
			pickerInfo.getPicker().createColorPickingTexture(pickerInfo);
			pickerInfo.getPicker().unbindFrameBuffer();
			pickerInfo = null;
			mPickerInfo = null;
			render();
		} else if (mPostProcessingRenderer.isEnabled()) {
			mPostProcessingRenderer.render();
		}

		for (int i = 0, j = mPlugins.size(); i < j; i++)
			mPlugins.get(i).render();
	}

	public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset) {
	}

	public void onTouchEvent(MotionEvent event) {

	}		

	public void onSurfaceChanged(GL10 gl, int width, int height) {
		mViewportWidth = width;
		mViewportHeight = height;
		mCamera.setProjectionMatrix(width, height);
		GLES20.glViewport(0, 0, width, height);
	}


	/* Called when the OpenGL context is created or re-created. Don't set up your scene here,
	 * use initScene() for that.
	 * 
	 * @see rajawali.renderer.RajawaliRenderer#initScene
	 * @see android.opengl.GLSurfaceView.Renderer#onSurfaceCreated(javax.microedition.khronos.opengles.GL10, javax.microedition.khronos.egl.EGLConfig)
	 * 
	 */
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {		
		supportsUIntBuffers = gl.glGetString(GL10.GL_EXTENSIONS).indexOf("GL_OES_element_index_uint") > -1;

		GLES20.glFrontFace(GLES20.GL_CCW);
		GLES20.glCullFace(GLES20.GL_BACK);

		if (!mSceneInitialized) {
			mTextureManager = new TextureManager(mContext);
			initScene();
		}

		if (!mSceneCachingEnabled) {
			mTextureManager.reset();
			if (mChildren.size() > 0) {
				mChildren.clear();
			}
			if (mPlugins.size() > 0) {
				mPlugins.clear();
			}
		} else if(mSceneCachingEnabled && mSceneInitialized) {
			mTextureManager.reload();
			reloadChildren();
			if(mSkybox != null)
				mSkybox.reload();
			if(mPostProcessingRenderer.isInitialized())
				mPostProcessingRenderer.reload();
			reloadPlugins();
			mReloadPickerInfo = true;
		}

		mSceneInitialized = true;
		startRendering();
	}

	private void reloadChildren() {
		for (int i = 0; i < mChildren.size(); i++)
			mChildren.get(i).reload();
	}

	private void reloadPlugins() {
		for (int i = 0, j = mPlugins.size(); i < j; i++)
			mPlugins.get(i).reload();
	}

	/**
	 * Scene construction should happen here, not in onSurfaceCreated()
	 */
	protected void initScene() {

	}

	protected void destroyScene() {
		mSceneInitialized = false;
		for (int i = 0; i < mChildren.size(); i++)
			mChildren.get(i).destroy();
		mChildren.clear();
		for (int i = 0, j = mPlugins.size(); i < j; i++)
			mPlugins.get(i).destroy();
		mPlugins.clear();
	}

	public void startRendering() {
		if (mTimer != null) {
			mTimer.cancel();
			mTimer.purge();
		}

		mTimer = new Timer();
		mTimer.schedule(new RequestRenderTask(), 0, (long) (1000 / mFrameRate));
	}

	/**
	 * Stop rendering the scene.
	 *
	 * @return true if rendering was stopped, false if rendering was already
	 *         stopped (no action taken)
	 */
	protected boolean stopRendering() {
		if (mTimer != null) {
			mTimer.cancel();
			mTimer.purge();
			mTimer = null;
			return true;
		}
		return false;
	}

	public void onVisibilityChanged(boolean visible) {
		if (!visible) {
			stopRendering();
		} else
			startRendering();
	}

	public void onSurfaceDestroyed() {
		stopRendering();
		TimerManager.getInstance().clear();
		if (mTextureManager != null)
			mTextureManager.reset();
		destroyScene();
	}

	public void setSharedPreferences(SharedPreferences preferences) {
		this.preferences = preferences;
	}

	private class RequestRenderTask extends TimerTask {
		public void run() {
			if (mSurfaceView != null) {
				mSurfaceView.requestRender();
			}
		}
	}

	public Number3D unProject(float x, float y, float z) {
		x = mViewportWidth - x;
		y = mViewportHeight - y;

		float[] m = new float[16], mvpmatrix = new float[16],
				in = new float[4],
				out = new float[4];

		Matrix.multiplyMM(mvpmatrix, 0, mPMatrix, 0, mVMatrix, 0);
		Matrix.invertM(m, 0, mvpmatrix, 0);

		in[0] = (x / (float)mViewportWidth) * 2 - 1;
		in[1] = (y / (float)mViewportHeight) * 2 - 1;
		in[2] = 2 * z - 1;
		in[3] = 1;

		Matrix.multiplyMV(out, 0, m, 0, in, 0);

		if (out[3]==0)
			return null;

		out[3] = 1/out[3];
		return new Number3D(out[0] * out[3], out[1] * out[3], out[2] * out[3]);
	}

	public float getFrameRate() {
		return mFrameRate;
	}

	public void setFrameRate(int frameRate) {
		setFrameRate((float)frameRate);
	}

	public void setFrameRate(float frameRate) {
		this.mFrameRate = frameRate;
		if (stopRendering()) {
			// Restart timer with new frequency
			startRendering();
		}
	}

	public float getRefreshRate() {
		return ((WindowManager) mContext
				.getSystemService(Context.WINDOW_SERVICE))
				.getDefaultDisplay()
				.getRefreshRate();
	}

	public WallpaperService.Engine getEngine() {
		return mWallpaperEngine;
	}

	public void setEngine(WallpaperService.Engine engine) {
		this.mWallpaperEngine = engine;
	}

	public GLSurfaceView getSurfaceView() {
		return mSurfaceView;
	}

	public void setSurfaceView(GLSurfaceView surfaceView) {
		this.mSurfaceView = surfaceView;
	}

	public Context getContext() {
		return mContext;
	}

	public TextureManager getTextureManager() {
		return mTextureManager;
	}

	public void addChild(BaseObject3D child) {
		mChildren.add(child);
	}

	public void clearChildren() {
		mChildren.clear();
	}

	public void addPlugin(IRendererPlugin plugin) {
		mPlugins.add(plugin);
	}

	public void clearPlugins() {
		mPlugins.clear();
	}

	protected void setSkybox(int resourceId) {
		mCamera.setFarPlane(1000);
		mSkybox = new Cube(700, true, false);
		mSkybox.setDoubleSided(true);
		mSkyboxTextureInfo = mTextureManager.addTexture(BitmapFactory.decodeResource(mContext.getResources(), resourceId));
		SimpleMaterial material = new SimpleMaterial();
		material.addTexture(mSkyboxTextureInfo);
		mSkybox.setMaterial(material);
	}

	protected void setSkybox(int front, int right, int back, int left, int up, int down) {
		mCamera.setFarPlane(1000);
		mSkybox = new Cube(700, true);

		Bitmap[] textures = new Bitmap[6];
		textures[0] = BitmapFactory.decodeResource(mContext.getResources(), left);
		textures[1] = BitmapFactory.decodeResource(mContext.getResources(), right);
		textures[2] = BitmapFactory.decodeResource(mContext.getResources(), up);
		textures[3] = BitmapFactory.decodeResource(mContext.getResources(), down);
		textures[4] = BitmapFactory.decodeResource(mContext.getResources(), front);
		textures[5] = BitmapFactory.decodeResource(mContext.getResources(), back);

		mSkyboxTextureInfo = mTextureManager.addCubemapTextures(textures);
		SkyboxMaterial mat = new SkyboxMaterial();
		mat.addTexture(mSkyboxTextureInfo);
		mSkybox.setMaterial(mat);
	}
	
	protected void updateSkybox(int resourceId) {
		mTextureManager.updateTexture(mSkyboxTextureInfo, BitmapFactory.decodeResource(mContext.getResources(), resourceId));
	}
	
	protected void updateSkybox(int front, int right, int back, int left, int up, int down) {
		Bitmap[] textures = new Bitmap[6];
		textures[0] = BitmapFactory.decodeResource(mContext.getResources(), left);
		textures[1] = BitmapFactory.decodeResource(mContext.getResources(), right);
		textures[2] = BitmapFactory.decodeResource(mContext.getResources(), up);
		textures[3] = BitmapFactory.decodeResource(mContext.getResources(), down);
		textures[4] = BitmapFactory.decodeResource(mContext.getResources(), front);
		textures[5] = BitmapFactory.decodeResource(mContext.getResources(), back);

		mTextureManager.updateCubemapTextures(mSkyboxTextureInfo, textures);
	}

	public boolean removeChild(BaseObject3D child) {
		return mChildren.remove(child);
	}

	public boolean removePlugin(IRendererPlugin plugin) {
		return mPlugins.remove(plugin);
	}

	public int getNumPlugins() {
		return mPlugins.size();
	}

	public List<IRendererPlugin> getPlugins() {
		return mPlugins;
	}

	public boolean hasPlugin(IRendererPlugin plugin) {
		return mPlugins.contains(plugin);
	}

	public int getNumChildren() {
		return mChildren.size();
	}

	public List<BaseObject3D> getChildren() {
		return mChildren;
	}

	protected boolean hasChild(BaseObject3D child) {
		return mChildren.contains(child);
	}

	public void addPostProcessingFilter(IPostProcessingFilter filter) {
		if(mFilters.size() > 0)
			mFilters.remove(0);
		mFilters.add(filter);
		mPostProcessingRenderer.setEnabled(true);
		mPostProcessingRenderer.setFilter(filter);
	}

	public void accept(INodeVisitor visitor) {
		visitor.apply(this);
		for (int i = 0; i < mChildren.size(); i++)
			mChildren.get(i).accept(visitor);
	}	

	public void removePostProcessingFilter(IPostProcessingFilter filter) {
		mFilters.remove(filter);
	}

	public void clearPostProcessingFilters() {
		mFilters.clear();
		mPostProcessingRenderer.unbind();
		mPostProcessingRenderer.destroy();
		mPostProcessingRenderer = new PostProcessingRenderer(this);
	}

	public int getViewportWidth() {
		return mViewportWidth;
	}

	public int getViewportHeight() {
		return mViewportHeight;
	}

	public void setBackgroundColor(float red, float green, float blue, float alpha) {
		mRed = red;
		mGreen = green;
		mBlue = blue;
		mAlpha = alpha;
	}

	public void setBackgroundColor(int color) {
		setBackgroundColor(Color.red(color) / 255f, Color.green(color) / 255f, Color.blue(color) / 255f, Color.alpha(color) / 255f);
	}

	public boolean getSceneInitialized() {
		return mSceneInitialized;
	}

	public void setSceneCachingEnabled(boolean enabled) {
		mSceneCachingEnabled = enabled;
	}

	public boolean getSceneCachingEnabled() {
		return mSceneCachingEnabled;
	}

	public void setFogEnabled(boolean enabled) {
		mFogEnabled = enabled;
		mCamera.setFogEnabled(enabled);
	}

	public void setUsesCoverageAa(boolean value) {
		mUsesCoverageAa = value;
	}

	public static boolean isFogEnabled() {
		return mFogEnabled;
	}

	public static int getMaxLights() {
		return mMaxLights;
	}

	public static void setMaxLights(int maxLights) {
		RajawaliRenderer.mMaxLights = maxLights;
	}

	public void setFPSUpdateListener(FPSUpdateListener listener) {
		mFPSUpdateListener = listener;
	}

	public static int checkGLError(String message) {
		int error = GLES20.glGetError();
		if(error != GLES20.GL_NO_ERROR)
		{
			StringBuffer sb = new StringBuffer();
			if(message != null)
				sb.append("[").append(message).append("] ");
			sb.append("GLES20 Error: ");
			sb.append(GLU.gluErrorString(error));
			RajLog.e(sb.toString());
		}
		return error;
	}

	public int getNumTriangles() {
		int triangleCount = 0;
		for (int i = 0; i < mChildren.size(); i++) {
			if (mChildren.get(i).getGeometry() != null && mChildren.get(i).getGeometry().getVertices() != null && mChildren.get(i).isVisible())
				triangleCount += mChildren.get(i).getGeometry().getVertices().limit() / 9;
		}
		return triangleCount;
	}
} public static final EvernoteMars EVERNOTE = new EvernoteMars(); public static MarsImagesApp MARS_IMAGES; public static final String TAG = "MarsImagesApplication";
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLES20;
import android.os.AsyncTask;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.view.View;
import com.evernote.edam.type.Note;
import com.powellware.marsimages.R;
import org.json.JSONArray;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import gov.nasa.jpl.hi.marsimages.EvernoteMars;
import gov.nasa.jpl.hi.marsimages.models.CameraModel;
import gov.nasa.jpl.hi.marsimages.models.ImageQuad;
import gov.nasa.jpl.hi.marsimages.models.M;
import gov.nasa.jpl.hi.marsimages.models.Model;
import gov.nasa.jpl.hi.marsimages.models.Quad;
import gov.nasa.jpl.hi.marsimages.rovers.Rover;
import rajawali.lights.PointLight;
import rajawali.materials.AMaterial;
import rajawali.materials.DiffuseMaterial;
import rajawali.materials.SimpleMaterial;
import rajawali.materials.TextureInfo;
import rajawali.materials.TextureManager;
import rajawali.primitives.Plane;
import rajawali.renderer.RajawaliRenderer;
import static android.opengl.GLES20.GL_LINE_LOOP;
import static gov.nasa.jpl.hi.marsimages.EvernoteMars.EVERNOTE;
import static gov.nasa.jpl.hi.marsimages.MarsImagesApp.MARS_IMAGES;
import static gov.nasa.jpl.hi.marsimages.MarsImagesApp.TAG;
        Log.d(TAG, "Gyro " + (gyroEnabled ? "True" : "False"));
    }

    public int[] getRMC() {
        return rmc;
    }

    public void setDeviceOrientationAngles(float deviceAzimuth, float devicePitch) {
        this.deviceAzimuth = deviceAzimuth;
        this.devicePitch = devicePitch;
    }

    public double getDeviceAzimuth() {
        return deviceAzimuth;
    }

    public double getDevicePitch() {
        return devicePitch;
    }

    public void incrementCameraMotion(float xMovement, float yMovement) {
        cameraRelativeXMotion += xMovement;
        cameraRelativeYMotion += yMovement;
    }

    @Override
    public void onSurfaceDestroyed() {
        super.onSurfaceDestroyed();
    }

    public void deleteImages() {
        notesInScene.clear();
        mSurfaceView.queueEvent(new Runnable() {
            @Override
            public void run() {
                for (ImageQuad photoQuad : photoQuads.values()) {
                    if (photoQuad.getMaterial() != null) {
                        ArrayList<TextureInfo> list = photoQuad.getTextureInfoList();
                        if (!list.isEmpty()) mTextureManager.removeTextures(list);
                    }
                }
                photoQuads.clear();
                clearChildren();
                addChild(plane);
            }
        });
    }

    private void prepareImageQuads() {

        float[] viewMatrix = mCamera.getViewMatrix();
        float[] projectionMatrix = mCamera.getProjectionMatrix();
        mCamera.updateFrustum(projectionMatrix, viewMatrix);

        for (String title: photoQuads.keySet()) {
            ImageQuad imageQuad = photoQuads.get(title);

//            if (!mCamera.mFrustum.sphereInFrustum(imageQuad.getBoundsCenter(), (float)imageQuad.getBoundsRadius())) { //This doesn't work. Bad on them.

            if (!imageQuad.cameraIsLookingAtMe(getCamera())) {
                imageQuad.setVisible(false);
                imageQuad.stopLoading();
                mTextureManager.removeTextures(imageQuad.getTextureInfoList());
                for (TextureInfo textureInfo : imageQuad.getTextureInfoList())
                    imageQuad.removeTexture(textureInfo);
                imageQuad.getTextureInfoList().clear();
                continue;
            }
            imageQuad.setVisible(true);
            if (imageQuad.getTextureInfoList().size() == 0 || mScaleChanged) {
                prepareImageQuad(imageQuad, title);
            }
        }
        mScaleChanged = false;
    }

    private void prepareImageQuad(ImageQuad imageQuad, String title) {
        Note photo = notesInScene.get(title);
        imageQuad.loadImageAndTexture(photo, title, computeIdealImageResolution(photo), this);
    }

    private final BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            final Rover rover = MARS_IMAGES.getMission();
            if (intent.getAction() == null) return;

            if (intent.getAction().equals(EvernoteMars.END_NOTE_LOADING)) {
                Integer notesReturned = intent.getIntExtra(EvernoteMars.NUM_NOTES_RETURNED, 0);
                Log.d("mosaic receiver", "PERF Notes returned: " + notesReturned);


                if (notesReturned > 0) {
                    EVERNOTE.loadMoreNotes(mContext, false);
                } else {
                    Log.d("mosaic receiver", "PERF Total notes returned: "+EVERNOTE.getNotes().size());
                    ((MosaicActivity)getContext()).getProgressBar().setVisibility(View.INVISIBLE);
                    new AsyncTask<Void, Void, List<QuadInitializer>>() {
                        @Override
                        protected List<QuadInitializer> doInBackground(Void... voids) {

                            if (!photoQuads.isEmpty())
                                return Collections.emptyList(); //we've already made the image quads

                            List<QuadInitializer> quadInitializers = new ArrayList<>();
//                            Log.d(TAG, "Making image quads");
                            List<Note> notes = EVERNOTE.getNotes();
                            Map<String, JSONArray> modelsForNotes = new HashMap<>();
                            for (Note note : notes) {
                                modelsForNotes.put(note.getTitle(), rover.modelJson(note));
                            }
                            binImagesByPointing(notes, modelsForNotes);
                            for (String photoTitle : notesInScene.keySet()) {
                                Note photo = notesInScene.get(photoTitle);

                                JSONArray model_json = modelsForNotes.get(photo.getTitle());
                                if (model_json == null)
                                    continue;

                                Model model = CameraModel.getModel(model_json);