content: package me.tagavari.airmessage.view;
  
  import android.content.Context;
  import android.content.res.Resources;
  import android.content.res.TypedArray;
  import android.graphics.*;
  import android.os.SystemClock;
  import android.util.AttributeSet;
  import android.view.TextureView;
  import android.view.View;
  import androidx.annotation.Nullable;
  import androidx.core.graphics.ColorUtils;
  import me.tagavari.airmessage.R;
  import me.tagavari.airmessage.flavor.CrashlyticsBridge;
  
  import java.util.Arrays;
  import java.util.Random;
  import java.util.concurrent.atomic.AtomicInteger;
  import java.util.concurrent.locks.Lock;
  import java.util.concurrent.locks.ReentrantLock;
  
  public class InvisibleInkView extends TextureView implements Runnable {
  	//Creating the reference variables
  	private static final int particleLifetime = 3 * 1000;
  	private static final int pixelsPerParticle = 150;
  	private static final float particleVelocity = 0.01F;
  	private static final float particleRadius = 0.7F;
  	
  	private static final int timeRevealTransition = 500; //0.5 seconds
  	private static final int timeRevealStay = 9 * 1000; //8 seconds
  	
  	//Creating the attribute values
  	private int backgroundColor;
  	
  	//Creating the drawing values
  	private final Paint backgroundPaint;
  	private final Paint particlePaint;
  	
  	//Creating the rendering values (values used in the drawing thread)
  	private final Lock viewRadiiLock = new ReentrantLock();
  	private float[] viewRadii = new float[8];
  	private final AtomicInteger viewWidth = new AtomicInteger();
  	private final AtomicInteger viewHeight = new AtomicInteger();
  	
  	//Creating the threading values
  	private Thread viewThread = null;
  	private boolean surfaceAvailable = false;
  	private volatile boolean viewRequestedRunning = false;
  	private volatile boolean viewRunning = false;
  	private volatile boolean viewUpdateRequested = false;
  	private volatile boolean viewRevealRequested = false;
  	private volatile boolean viewRevealRunning = false;
  	
  	private boolean firstLayoutPassCompleted = false;
  	private boolean firstStartRequestCompleted = false;
  	
  	//Creating the other values
  	private final Random random = new Random();
  	private int visiblePixelCount;
  	//private boolean[][] visibilityMap;
  	private Particle[] particleList = null;
  	private float particleVelocityPx;
  	private float particleRadiusPx;
  	private long lastTimeCheck = getTime();
  	
  	public InvisibleInkView(Context context, @Nullable AttributeSet attrs) {
  		super(context, attrs);
  		
  		//Configuring the view
  		setOpaque(false);
  		
  		//Getting the attributes
  		TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.InvisibleInkView, 0, 0);
  		
  		try {
  			backgroundColor = attributes.getColor(R.styleable.InvisibleInkView_backgroundColor, Color.TRANSPARENT);
  		} finally {
  			attributes.recycle();
  		}
  		
  		//Converting the units
  		particleVelocityPx = dpToPx(particleVelocity);
  		particleRadiusPx = dpToPx(particleRadius);
  		
  		//Setting up the paints
  		backgroundPaint = new Paint();
  		backgroundPaint.setStyle(Paint.Style.FILL);
  		backgroundPaint.setColor(backgroundColor);
  		
  		particlePaint = new Paint();
  		particlePaint.setAntiAlias(true);
  		particlePaint.setStyle(Paint.Style.FILL);
  		
  		//Setting the surface listener
  		setSurfaceTextureListener(new SurfaceTextureListener() {
  			@Override
  			public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
  				//Setting the drawing surface as available
  				surfaceAvailable = true;
  				
  				//Starting the drawing thread
  				if(viewRunning && firstLayoutPassCompleted && viewThread == null) startThread();
  			}
  			
  			@Override
  			public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
  			
  			}
  			
  			@Override
  			public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
  				surfaceAvailable = false;
  				onPause();
  				return true;
  			}
  			
  			@Override
  			public void onSurfaceTextureUpdated(SurfaceTexture surface) {
  			
  			}
  		});
  	}
  	
  	@Override
  	public void run() {
  		Canvas canvas;
  		float[] drawParticlePositions = new float[2];
  		float drawParticleProgress;
  		
  		Path clipPath = new Path();
  		RectF clipRect = new RectF();
  		
  		int revealTime = 0;
  		int startAlpha = 0xFF;
  		int targetAlpha = startAlpha;
  		
  		try {
  			while(viewRunning) {
  				//Returning if the view isn't valid
  				if(!isAttachedToWindow() || !isAvailable()) return;
  				
  				//Getting the canvas
  				canvas = lockCanvas();
  				try {
  					if(canvas == null) return;
  					
  					//Checking if the view needs to be updated
  					if(viewUpdateRequested) {
  						processTargetView();
  						viewUpdateRequested = false;
  					}
  					
  					//Returning if the view isn't valid
  					if(!isAttachedToWindow() || !isAvailable()) return;
  					
  					//Calculating the time difference
  					long currentTime = getTime();
  					int timeDiff = (int) (currentTime - lastTimeCheck);
  					lastTimeCheck = currentTime;
  					
  					//Advancing the reveal time
  					if(revealTime > 0) {
  						revealTime = Math.max(revealTime - timeDiff, 0);
  						if(revealTime > timeRevealTransition + timeRevealStay) targetAlpha = lerpInt(0x00, startAlpha, (revealTime - (timeRevealTransition + timeRevealStay)) / (float) timeRevealTransition); //Fade out stage
  						else if(revealTime > timeRevealTransition) targetAlpha = 0x00; //Stay stage
  						else targetAlpha = lerpInt(0xFF, 0x00, revealTime / (float) timeRevealTransition); //Fade in stage
  					} else viewRevealRunning = false;
  					
  					//Checking if a reveal had been requested
  					if(viewRevealRequested) {
  						//Setting the reveal time
  						revealTime = timeRevealTransition * 2 + timeRevealStay;
  						startAlpha = targetAlpha;
  						
  						viewRevealRequested = false;
  						viewRevealRunning = true;
  					}
  					
  					//Clearing the canvas
  					if(!viewRunning) return;
  					canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
  					
  					//Rounding out the view
  					clipPath.reset();
  					clipRect.set(0, 0, viewWidth.get(), viewHeight.get());
  					
  					viewRadiiLock.lock();
  					try {
  						clipPath.addRoundRect(clipRect, viewRadii, Path.Direction.CW);
  					} finally {
  						viewRadiiLock.unlock();
  					}
  					
  					if(!viewRunning) return;
  					canvas.clipPath(clipPath);
  					
  					//Drawing the background
  					if(!viewRunning) return;
  					if(backgroundColor != Color.TRANSPARENT) {
  						backgroundPaint.setColor(ColorUtils.setAlphaComponent(backgroundColor, targetAlpha));
  						canvas.drawPaint(backgroundPaint);
  					}
  					
  					//Drawing the particles
  					if(particleList == null) continue;
  					for(Particle particle : particleList) {
  						//Getting the particle information
  						drawParticleProgress = particle.calculateFrame(timeDiff, drawParticlePositions);
  						
  						//Configuring the paint
  						particlePaint.setColor(Color.argb(Math.max(calculateAlpha(drawParticleProgress) - (0xFF - targetAlpha), 0), 0xFF, 0xFF, 0xFF));
  						
  						if(!viewRunning) return;
  						canvas.drawCircle(drawParticlePositions[0] * (float) viewWidth.get(), drawParticlePositions[1] * (float) viewHeight.get(), particleRadiusPx, particlePaint);
  					}
  				} finally {
  					//Updating the canvas changes
  					unlockCanvasAndPost(canvas);
  				}
  			}
  		} catch(Exception exception) {
  			exception.printStackTrace();
  			CrashlyticsBridge.recordException(exception);
  		} finally {
  			stop();
  		}
  	}
  	
  	@Override
  	protected void onAttachedToWindow() {
  		super.onAttachedToWindow();
  		onResume();
  	}
  	
  	@Override
  	protected void onDetachedFromWindow() {
  		super.onDetachedFromWindow();
  		onPause();
  	}
  	
  	@Override
  	protected void onVisibilityChanged(View changedView, int visibility) {
  		super.onVisibilityChanged(changedView, visibility);
  		
  		if(visibility == VISIBLE) {
  			if(viewRequestedRunning) start();
  		} else stop();
  	}
  	
  	private static int calculateAlpha(float progress) {
  		if(progress < 0.1F) return (int) (progress * 10F * 255F);
  		else return 255 - (int) ((progress - 0.1F) * (10F / 9F) * 255F);
  	}
  	
  	@Override
  	protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
  		if(isInEditMode()) return;
  		
  		//Setting the view sizes
  		viewWidth.set(width);
  		viewHeight.set(height);
  		
  		//Calculating the visible pixel count
  		visiblePixelCount = width * height;
  		
  		//Setting the layout pass as completed
  		boolean isFirst = !firstLayoutPassCompleted;
  		firstLayoutPassCompleted = true;
  		
  		//Returning if the isn't running
  		if(!viewRunning) return;
  		
  		//Requesting a view update
  		viewUpdateRequested = true;
  		
  		//Starting the thread if start() was called before this layout pass (and was unable to start because of a lack of layout data)
  		if(isFirst && surfaceAvailable && firstStartRequestCompleted) startThread();
  	}
  	
  	void processTargetView() {
  		//Rebuilding the particle list
  		int targetListSize = visiblePixelCount / pixelsPerParticle;
  		if(particleList == null) {
  			//Populating the particle list
  			particleList = new Particle[targetListSize];
  			for(int i = 0; i < targetListSize; i++) particleList[i] = new Particle();
  		} else {
  			int listSize = particleList.length;
  			if(listSize != targetListSize) {
  				//Resizing the array
  				particleList = Arrays.copyOf(particleList, targetListSize);
  				
  				//Filling in the blank values
  				for(int i = listSize; i < targetListSize; i++) particleList[i] = new Particle();
  			}
  		}
  	}
  	
  	public void setState(boolean state) {
  		if(state) start();
  		else stop();
  		viewRequestedRunning = state;
  	}
  	
  	private void start() {
  		//Returning if the view is already running
  		if(viewRunning) return;
  		
  		//Setting the view as running
  		viewRunning = true;
  		
  		//Requesting a view update
  		viewUpdateRequested = true;
  		
  		//Setting the start request as completed
  		firstStartRequestCompleted = true;
  		
  		//Starting the thread if there has already been a layout pass
  		if(firstLayoutPassCompleted && surfaceAvailable) startThread();
  	}
  	
  	private void stop() {
  		//Returning if the view isn't running
  		if(!viewRunning) return;
  		
  		//Setting the view as stopped
  		viewRunning = false;
  		
  		try {
  			if(viewThread != null) viewThread.join();
  		} catch(InterruptedException exception) {
  			exception.printStackTrace();
  		} finally {
  			viewThread = null;
  		}
  	}
  	
  	private void startThread() {
  		viewThread = new Thread(this);
  		viewThread.start();
  	}
  	
  	public void onPause() {
  		stop();
  	}
  	
  	public void onResume() {
  		if(viewRequestedRunning) start();
  	}
  	
  	public boolean reveal() {
  		viewRevealRequested = true;
  		return viewRevealRunning;
  	}
  	
  	public void setRadii(float topLeft, float topRight, float bottomRight, float bottomLeft) {
  		//Setting the radii
  		viewRadiiLock.lock();
  		try {
  			viewRadii = new float[]{topLeft, topLeft,
  					topRight, topRight,
  					bottomRight, bottomRight,
  					bottomLeft, bottomLeft};
  		} finally {
  			viewRadiiLock.unlock();
  		}
  	}
  	
  	public void setRadii(float[] radii) {
  		//Setting the radii
  		viewRadiiLock.lock();
  		try {
  			viewRadii = radii;
  		} finally {
  			viewRadiiLock.unlock();
  		}
  	}
  	
  	public void setBackgroundColor(int color) {
  		//Setting the background color
  		backgroundColor = color;
  	}
  	
  	/**
  	 * A class that represents a particle of the invisible ink
  	 * The coordinates represent values 0.0 to 1.0, mapped to the view's size
  	 * The creation date is used for timeLived
  	 */
  	private class Particle {
  		float x, y;
  		float velX, velY;
  		int cycleTime;
  		
  		Particle() {
  			//Prewarming
  			generateState();
  			cycleTime = random.nextInt(particleLifetime);
  		}
  		
  		void generateState() {
  			//Picking a random location
  			x = random.nextFloat();
  			y = random.nextFloat();
  			
  			//Picking a new velocity
  			float direction = random.nextFloat() * (float) Math.PI * 2F;
  			velX = (float) Math.cos(direction) - (float) Math.sin(direction);
  			velY = (float) Math.sin(direction) + (float) Math.cos(direction);
  		}
  		
  		private void cycleParticle(int timeDiff) {
  			//Adding to the cycle time
  			int newTime = cycleTime + timeDiff;
  			if(newTime >= particleLifetime) {
  				//Cycling the time
  				cycleTime = newTime % particleLifetime;
  				
  				//Regenerating the state
  				generateState();
  				
  				
  			} else cycleTime = newTime;
  		}
  		
  		/**
  		 * Calculates the values required for the frame, recycling the particle if it has exceeded its timeLived
  		 * @param locOut The X and the Y location of the particle
  		 * @return The progress of the particle (timeLived)
  		 */
  		float calculateFrame(int timeDiff, float[] locOut) {
  			//Cycling the particle (making a new particle with the current instance) if needed
  			cycleParticle(timeDiff);
  			
  			//Calculating the progress (from life to death - 0.0 to 1.0)
  			float progress = (float) cycleTime / (float) particleLifetime;
  			
  			//Calculating the position
  			locOut[0] = x + velX * particleVelocityPx * progress;
  			locOut[1] = y + velY * particleVelocityPx * progress;
  			
  			//Returning the progress
  			return progress;
  		}
  	}
  	
  	private static long getTime() {
  		return SystemClock.uptimeMillis();
  	}
  	
  	private static float dpToPx(float dp) {
  		return dp * Resources.getSystem().getDisplayMetrics().density;
  	}
  	
  	private static float pxToDp(float px) {
  		return px / Resources.getSystem().getDisplayMetrics().density;
  	}
  	
  	private static int lerpInt(int start, int end, float progress) {
  		return start + (int) ((end - start) * progress);
  	}
  }