/*
	Copyright © XlXi 2022
*/

import { Component, createRef } from 'react';

import classNames from 'classnames/bind';

import axios from 'axios';

import { buildGenericApiUrl } from '../util/HTTP.js';
import Loader from './Loader';

axios.defaults.withCredentials = true;

class DeploymentUploadModal extends Component {
	constructor(props) {
		super(props);
		this.state = {
			deployments: []
		};
		
		this.updateInterval = null;
		
		this.ModalRef = createRef();
		this.Modal = null;
	}
	
	componentDidMount() {
		this.Modal = new Bootstrap.Modal(
			this.ModalRef.current,
			{
				backdrop: 'static',
				keyboard: false
			}
		);
		this.Modal.show();
		
		this.ModalRef.current.addEventListener('hidden.bs.modal', (event) => {
			this.props.setModal(null);
		})
		
		this.setState({ deployments: this.props.deployments });
		this.updateInterval = setInterval(function() {
			let deployments = this.state.deployments;
			deployments.map((component, index) => {
				axios.get(buildGenericApiUrl('api', `admin/v1/deploy?version=${ component.version }`))
					.then(res => {
						deployments[index] = { ...component, ...res.data };
						
						// XlXi: -_-
						//deployment['status'] = res.data['status'];
						//deployment['message'] = res.data['message'];
						//deployment['progress'] = res.data['progress'];
					});
			});
			this.setState({ deployments: deployments });
		}.bind(this), 2000);
	}
	
	componentWillUnmount() {
		this.Modal.dispose();
		
		clearInterval(this.updateInterval);
	}
	
	render() {
		return (
			<div ref={this.ModalRef} className="modal fade">
				<div className="modal-dialog modal-dialog-centered">
					<div className="modal-content">
						<div className="modal-header">
							<h5 className="modal-title">Deployment Status</h5>
							<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
						</div>
						<div className="modal-body">
							{
								this.state.deployments.map((deployment, index) => (
									<>
										<h5 className="mb-0">Deploying { capitalizeFirstLetter(deployment.key) } { deployment.version }</h5>
										<p className="text-muted mb-2">{ deployment.message }</p>
										<div className="progress">
											<div
												className={classNames({
													'progress-bar': true,
													'progress-bar-striped': true,
													'progress-bar-animated': true,
													'bg-primary': ( deployment.status == 'Loading' ),
													'bg-success': ( deployment.status == 'Success' ),
													'bg-danger': ( deployment.status == 'Error' )
												})}
												style={ {width: `${deployment.progress * 100}%`} }
											></div>
										</div>
										{ index != this.state.deployments.length-1 ? <hr /> : null }
									</>
								))
							}
						</div>
					</div>
				</div>
			</div>
		);
	}
}

class DeploymentCard extends Component {
	constructor(props) {
		super(props);
	}
	
	render() {
		return (
			<div className="card mb-2">
				<div className="card-header d-flex">
					<span>{ this.props.name }</span>
					<button className="ms-auto btn-close" onClick={ ()=>this.props.removeDeployment(this.props.index) }></button>
				</div>
				<div className="card-body">
					{ this.props.children }
				</div>
			</div>
		);
	}
}

class RevertDeploymentCard extends Component {
	constructor(props) {
		super(props);
		this.state = {
			loading: true,
			deployments: []
		};
	}
	
	componentDidMount() {
		let deployType = 'Unknown';
		switch(this.props.index)
		{
			case 'client':
				deployType = 'WindowsPlayer'
				break;
			case 'studio':
				deployType = 'Studio'
				break;
		}
		
		let deployHistoryRegex = new RegExp(`New ${deployType} version-[a-zA-Z0-9]{16} at \\d{1,2}\\/\\d{1,2}\\/\\d{4} \\d{1,2}:\\d{1,2}:\\d{1,2} (AM|PM), file version: (\\d+(, )?){4}\\.{3}Done!`, 'g');
		
		axios.get(buildGenericApiUrl('setup', 'DeployHistory.txt'))
			.then(res => {
				let deployments = res.data.split(/\r?\n/).reverse();
				deployments = deployments.filter((deployment) => deployHistoryRegex.test(deployment)).slice(0, 30);
				
				let realDeployments = [];
				
				deployments.map((deployment) => {
					let newDeployment = {
						type: deployType,
						hash: deployment.match(/version-[a-zA-Z0-9]{16}/)[0],
						version: deployment.match(/file version: (\d+(, )?){4}/)[0].replace('file version: ', ''),
						date: deployment.match(/at \d{1,2}\/\d{1,2}\/\d{4} \d{1,2}:\d{1,2}:\d{1,2} (AM|PM)/)[0].replace('at ', '')
					};
					
					realDeployments.push(newDeployment);
				});
				
				this.setState({ loading: false, deployments: realDeployments });
			});
	}
	
	render() {
		return (
			<DeploymentCard name={ this.props.name } index={ this.props.index } removeDeployment={ this.props.removeDeployment }>
				<h5 className="mb-0">Revert Deployment</h5>
				<p className="text-muted">Select a previous deployment below to roll back the { this.props.index } version.</p>
				<select className="form-select mt-2" id="vb-revert-deployment" disabled={ this.state.loading }>
					<option selected>{ this.state.loading ? 'Loading...' : 'None Selected' }</option>
					{
						this.state.deployments.map((deployment, index) => (
							<option value={ deployment.hash } key={ index }>[{ deployment.type } { deployment.version }] [{ deployment.date }] { deployment.hash }</option>
						))
					}
				</select>
			</DeploymentCard>
		);
	}
}

class PushDeploymentCard extends Component {
	constructor(props) {
		super(props);
		this.state = {
			drag: false,
			showRequiredFiles: false,
			files: [],
			options: []
		};
		
		this.dragDropBox = createRef();
		this.dragCounter = 0;
		
		this.neededFiles = [
			'content-fonts.zip',
			'content-music.zip',
			'content-particles.zip',
			'content-sky.zip',
			'content-sounds.zip',
			'content-terrain.zip',
			'content-textures.zip',
			'content-textures2.zip',
			'content-textures3.zip',
			'shaders.zip',
			'redist.zip',
			'Libraries.zip'
		];
		
		this.handleDrag = this.handleDrag.bind(this);
		this.handleDragIn = this.handleDragIn.bind(this);
		this.handleDragOut = this.handleDragOut.bind(this);
		this.handleDrop = this.handleDrop.bind(this);
		this.handleDropUi = this.handleDropUi.bind(this);
		this.fileExists = this.fileExists.bind(this);
		this.removeFile = this.removeFile.bind(this);
		this.setOptions = this.setOptions.bind(this);
	}
	
	componentDidMount() {
		switch(this.props.index)
		{
			case 'client':
				this.neededFiles = this.neededFiles.concat([
					'PlayerPdb.zip',
					'VirtuBrick.zip',
					'VirtuBrickPlayerLauncher.exe'
				]);
				break;
			case 'studio':
				this.neededFiles = this.neededFiles.concat([
					'BuiltInPlugins.zip',
					'imageformats.zip',
					'content-scripts.zip',
					'StudioPdb.zip',
					'VirtuBrickStudio.zip',
					'VirtuBrickStudioLauncherBeta.exe'
				]);
				break;
		}
		
		this.setState({ showRequiredFiles: true });
		
		let ddb = this.dragDropBox.current
		ddb.addEventListener('dragenter', this.handleDragIn)
		ddb.addEventListener('dragleave', this.handleDragOut)
		ddb.addEventListener('dragover', this.handleDrag)
		ddb.addEventListener('drop', this.handleDrop)
		
		
	}
	
	componentWillUnmount() {
		let ddb = this.dragDropBox.current
		ddb.removeEventListener('dragenter', this.handleDragIn)
		ddb.removeEventListener('dragleave', this.handleDragOut)
		ddb.removeEventListener('dragover', this.handleDrag)
		ddb.removeEventListener('drop', this.handleDrop)
	}
	
	handleDrag(evt) {
		evt.preventDefault();
		evt.stopPropagation();
	}
	
	handleDragIn(evt) {
		evt.preventDefault();
		evt.stopPropagation();
		this.dragCounter++;
		if (evt.dataTransfer.items && evt.dataTransfer.items.length > 0) {
			this.setState({drag: true});
		}
	}
	
	handleDragOut(evt) {
		evt.preventDefault();
		evt.stopPropagation();
		this.dragCounter--;
		if (this.dragCounter === 0) {
			this.setState({drag: false});
		}
	}
	
	handleDrop(evt) {
		evt.preventDefault();
		evt.stopPropagation();
		this.setState({drag: false});
		if (evt.dataTransfer.files && evt.dataTransfer.files.length > 0) {
			this.handleDropUi(evt.dataTransfer.files);
			evt.dataTransfer.clearData();
			this.dragCounter = 0;
		}
	}
	
	handleDropUi(files) {
		let fileList = this.state.files;
		for (var i = 0; i < files.length; i++) {
			let file = files[i];
			
			if (!file)
				continue;
			
			if(this.fileExists(file.name))
				continue;
			
			fileList.push(file);
		}
		
		this.setState({files: fileList});
		this.props.updateDeploymentFiles(fileList);
	}
	
	fileExists(fileName) {
		return this.state.files.some(file => file.name.toLowerCase() === fileName.toLowerCase());
	}
	
	removeFile(fileName) {
		this.setState(prevState => ({
			files: prevState.files.filter((file) => file.name.toLowerCase() !== fileName.toLowerCase())
		}));
		
		this.props.updateDeploymentFiles(this.state.files);
	}
	
	setOptions(key, value) {
		let options = this.state.options;
		if(value != '')
			options[key] = value;
		else
			delete options[key];
		
		this.setState({ options: options });
		
		this.props.updateDeploymentOptions(options)
	}
	
	render() {
		return (
			<DeploymentCard name={ this.props.name } index={ this.props.index } removeDeployment={ this.props.removeDeployment }>
				<h5 className="mb-0">Deployment Files</h5>
				<p className="text-muted">Drag-and-Drop the necessary files into the box below. Any unneeded files will be discarded when uploading.</p>
				<div className="card bg-secondary mt-3 p-3" ref={ this.dragDropBox }>
					<div>
						{/* XlXi: Reusing game cards here because they were already exactly what I wanted. */}
						{
							this.state.files.length == 0
							?
							<p className="text-muted">Drop files here.</p>
							:
							this.state.files.map((file, index) => {
								let fileType = /(?:\.([^.]+))?$/.exec(file.name)[1].toLowerCase();
								let fileIconClasses = {
									'm-auto': true,
									'fs-1': true,
									'fa-regular': true
								};
								switch(fileType)
								{
									case 'exe':
										fileIconClasses['fa-browser'] = true;
										break;
									case 'zip':
										fileIconClasses['fa-file-zipper'] = true;
										break;
									default:
										fileIconClasses['fa-file'] = true;
										break;
								}
								
								return (
									<div className="virtubrick-item-card virtubrick-game-card">
										<div className="card m-2" data-bs-toggle="tooltip" data-bs-placement="top" title={ file.name }>
											<div className="bg-light d-flex p-3">
												<i className={classNames(fileIconClasses)}></i>
											</div>
											<div className="p-2">
												<p className="text-truncate">{ file.name }</p>
												<button className="btn btn-sm btn-danger mt-1 w-100" onClick={ ()=>this.removeFile(file.name) }>Remove</button>
											</div>
										</div>
									</div>
								);
							})
						}
					</div>
					{
						this.state.showRequiredFiles
						?
						<>
							<hr />
							<h5>Needed Files</h5>
							<div className="small">
								{
									this.neededFiles.map((fileName) => {
										let fileExists = this.fileExists(fileName);
										
										return (
											<p className={classNames({
												'text-success': fileExists,
												'text-danger': !fileExists
											})}>{ fileName }</p>
										);
									})
								}
							</div>
						</>
						:
						null
					}
				</div>
				{
					this.props.index == 'client'
					?
					<>
						<h5 className="mb-0 mt-3">Optional Configuration</h5>
						<p className="text-muted mb-3">Only change if you've updated the security settings on the client/rcc. Shutting down game servers will delay deployment by 10 minutes.</p>
						<div className="form-check form-switch">
							<input className="form-check-input" type="checkbox" role="switch" id="vb-shut-down-servers" />
							<label className="form-check-label" htmlFor="vb-shut-down-servers">Shut down game servers.</label>
						</div>
						<label htmlFor="vb-rcc-security-key" className="form-label mt-2">Update RCC Security Key</label>
						<input type="text" id="vb-rcc-security-key" className="form-control" placeholder="New RCC Security Key" onChange={ changeEvent => this.setOptions('rccAccessKey', changeEvent.target.value) } />
						<label htmlFor="vb-rcc-security-key" className="form-label mt-2">Update Version Compatibility Salt</label>
						<input type="text" id="vb-rcc-security-key" className="form-control" placeholder="New Version Compatibility Salt" onChange={ changeEvent => this.setOptions('versionCompatiblityFuzzyKey', changeEvent.target.value) } />
					</>
					:
					null
				}
			</DeploymentCard>
		);
	}
}

// https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript
function capitalizeFirstLetter(string) {
	return string.charAt(0).toUpperCase() + string.slice(1);
}

class Deployer extends Component {
	constructor(props) {
		super(props);
		this.state = {
			showModal: false,
			deployType: 'deploy',
			showTypeSwitchError: false,
			deploying: false,
			deployments: []
		};
		
		this.visibleModal = null;
		
		this.updateDeploymentFiles = this.updateDeploymentFiles.bind(this);
		this.deploymentExists = this.deploymentExists.bind(this);
		this.getDeployment = this.getDeployment.bind(this);
		this.createDeployment = this.createDeployment.bind(this);
		this.removeDeployment = this.removeDeployment.bind(this);
		this.trySetDeployType = this.trySetDeployType.bind(this);
		this.uploadDeploymentFiles = this.uploadDeploymentFiles.bind(this);
		this.uploadDeployments = this.uploadDeployments.bind(this);
		this.setModal = this.setModal.bind(this);
	}
	
	updateDeploymentFiles(key, files) {
		if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
			return;
		
		let deployment = this.getDeployment(key);
		deployment['files'] = files;
	}
	
	updateDeploymentOptions(key, options) {
		if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
			return;
		
		let deployment = this.getDeployment(key);
		deployment['extraOptions'] = options;
	}
	
	deploymentExists(key) {
		return this.state.deployments.some(deployment => deployment.key === key);
	}
	
	getDeployment(key) {
		return this.state.deployments.find((deployment) => deployment.key === key);
	}
	
	createDeployment(key) {
		if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
			return;
		
		let newElement = {
			key: key,
			name: capitalizeFirstLetter(this.state.deployType) + ' ' + capitalizeFirstLetter(key),
			files: [],
			extraOptions: []
		};
		
		this.setState(prevState => ({
			deployments: [...prevState.deployments, newElement]
		}));
	}
	
	removeDeployment(key) {
		if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
			return;
		
		this.setState(prevState => ({
			deployments: prevState.deployments.filter((deployment) => deployment.key !== key)
		}));
	}
	
	trySetDeployType(evt, type) {
		if(this.state.deploying) // XlXi: Prevent messing with the component when we're deploying.
			return;
		
		if(!evt.target.checked)
			return;
		
		if(this.state.deployType != type && this.state.deployments.length != 0)
		{
			this.setState({ showTypeSwitchError: true });
			return;
		}
		
		this.setState({ deployType: type, showTypeSwitchError: false });
	}
	
	uploadDeploymentFiles(key, version) {
		let deployment = this.getDeployment(key);
		let bodyFormData = new FormData();
		
		deployment.files.map((file) => {
			bodyFormData.append('file[]', file);
		});
		
		Object.keys(deployment.extraOptions).forEach(function(key, index) {
			bodyFormData.append(key, deployment.extraOptions[key]);
		});
		
		axios.post(
			buildGenericApiUrl('api', `admin/v1/deploy/${ deployment.version }`),
			bodyFormData
		);
	}
	
	uploadDeployments() {
		if(this.state.deploying) // XlXi: Prevent multiple deployments of the same files.
			return;
		
		this.setState({ deploying: true });
		
		let requests = 0;
		this.state.deployments.map((component, index) => {
			axios.get(buildGenericApiUrl('api', `admin/v1/deploy?type=deploy&app=${ component.key }`))
				.then(res => {
					requests++;
					
					this.state.deployments[index] = {...component, ...res.data};
					
					if(requests == this.state.deployments.length)
						this.setModal(<DeploymentUploadModal setModal={ this.setModal } deployments={ this.state.deployments } />);
					
					this.uploadDeploymentFiles(component.key, component.version);
				});
		});
		
		//this.setState({ deployments: [] });
	}
	
	setModal(modal = null) {
		this.visibleModal = modal;
		
		if(modal) {
			this.setState({'showModal': true});
		} else {
			this.setState({'showModal': false});
		}
	}
	
	render() {
		return (
			<>
				<h5>Deployment Options</h5>
				<div className="d-block">
					<div className="btn-group mb-1">
						<input type="radio" className="btn-check" name="vb-deployment-type" id="vb-deployment-deploy" autoComplete="off" onChange={ (e)=>this.trySetDeployType(e, 'deploy') } checked={ this.state.deployType == 'deploy' } />
						<label className="btn btn-sm btn-outline-primary" htmlFor="vb-deployment-deploy">Deploy</label>
						<input type="radio" className="btn-check" name="vb-deployment-type" id="vb-deployment-revert" autoComplete="off" onChange={ (e)=>this.trySetDeployType(e, 'revert') } checked={ this.state.deployType == 'revert' } />
						<label className="btn btn-sm btn-outline-primary" htmlFor="vb-deployment-revert">Revert</label>
					</div>
					<br />
					<button className="btn btn-sm btn-success" onClick={ ()=>this.createDeployment('client') } disabled={ this.deploymentExists('client') }>Deploy Client</button>
					<button className="btn btn-sm btn-primary" onClick={ ()=>this.createDeployment('studio') } disabled={ this.deploymentExists('studio') }>Deploy Studio</button>
				</div>
				<hr />
				{
					this.state.showTypeSwitchError ?
					<div className="alert alert-danger virtubrick-alert virtubrick-error-popup">Remove your { this.state.deployType == 'deploy' ? 'deployments' : 'reversions' } to change the uploader type.</div>
					:
					null
				}
				<h5>{ this.state.deployType == 'deploy' ? 'Staged Deployments' : 'Revert Deployments' }</h5>
				{
					this.state.deployments.length == 0 ?
					<p className="text-muted">No deployments are selected.</p>
					:
					this.state.deployments.map((component) => {
						// XlXi: surely theres a better way to do this.....
						switch(this.state.deployType)
						{
							case 'deploy':
								return <PushDeploymentCard name={ component.name } index={ component.key } key={ component.key } removeDeployment={ this.removeDeployment } updateDeploymentFiles={ (files)=>this.updateDeploymentFiles(component.key, files) } updateDeploymentOptions={ (options)=>this.updateDeploymentOptions(component.key, options) } />
							case 'revert':
								return <RevertDeploymentCard name={ component.name } index={ component.key } key={ component.key } removeDeployment={ this.removeDeployment } />
						}
					})
				}
				<hr />
				<button className="btn btn-primary" onClick={ this.uploadDeployments } disabled={ this.state.deploying || this.state.deployments.length == 0 }>{ this.state.deployType == 'deploy' ? 'Deploy' : 'Revert Deployment' }</button>
				{ this.state.showModal ? this.visibleModal : null }
			</>
		);
	}
}

export default Deployer;