import { isAbsolute, resolve, dirname, basename } from 'path';
import { createWriteStream, writeFileSync } from 'fs';
import { get } from 'http';

import * as plantumlEncoder from 'plantuml-encoder';
import { ConnectionOptionsReader, getConnectionManager, Connection } from 'typeorm';

import { Flags, Format, SkinParams } from '../types';
import { UmlBuilder } from './uml-builder.class';
import { Styles } from './styles.class';
import { MonochromeStyles, TextStyles } from './styles';

export class TypeormUml {

	/**
	 * Builds UML diagram.
	 *
	 * @async
	 * @public
	 * @param {string|Connection} configNameOrConnection The typeorm config filename or connection instance.
	 * @param {Flags} flags Build flags.
	 * @returns {string} Diagram URL or UML code depending on selected format.
	 */
	public async build( configNameOrConnection: string | Connection, flags: Flags ) : Promise<string> {
		const styles = this.getStyles( flags );
		const connection: Connection = typeof configNameOrConnection === 'string'
			? await this.getConnection( configNameOrConnection, flags )
			: configNameOrConnection;

		const builder = new UmlBuilder( connection, flags, styles );
		const uml = await builder.buildUml();

		if ( connection.isConnected ) {
			await connection.close();
		}

		if ( flags.format === Format.PUML ) {
			if ( flags.download ) {
				const path = this.getPath( flags.download );
				writeFileSync( path, uml );
			} else if ( flags.echo ) {
				process.stdout.write( `${ uml }\n` );
			}

			return uml;
		}

		const url = this.getUrl( uml, flags );
		if ( flags.download ) {
			await this.download( url, flags.download );
		} else if ( flags.echo ) {
			process.stdout.write( `${ url }\n` );
		}

		return url;
	}

	/**
	 * Creates and returns Typeorm connection based on selected configuration file.
	 *
	 * @async
	 * @private
	 * @param {string} configPath A path to Typeorm config file.
	 * @param {Flags} flags An object with command flags.
	 * @returns {Connection} A connection instance.
	 */
	private async getConnection( configPath: string, flags: Flags ): Promise<Connection> {
		let root = process.cwd();
		let configName = configPath;

		if ( isAbsolute( configName ) ) {
			root = dirname( configName );
			configName = basename( configName );
		}

		const cwd = dirname( resolve( root, configName ) );
		process.chdir( cwd );

		const connectionOptionsReader = new ConnectionOptionsReader( { root, configName } );
		const connectionOptions = await connectionOptionsReader.get( flags.connection || 'default' );
		return getConnectionManager().create( connectionOptions );
	}

	/**
	 * Builds a plantuml URL and returns it.
	 *
	 * @private
	 * @param {string} uml The UML diagram.
	 * @param {Flags} flags An object with command flags.
	 * @returns {string} A plantuml string.
	 */
	private getUrl( uml: string, flags: Flags ): string {
		const encodedUml = plantumlEncoder.encode( uml );

		const format = encodeURIComponent( flags.format );
		const schema = encodeURIComponent( encodedUml );
		const plantumlUrl = flags['plantuml-url'] || 'http://www.plantuml.com/plantuml';

		return `${ plantumlUrl.replace( /\/$/, '' ) }/${ format }/${ schema }`;
	}

	/**
	 * Downloads image into a file.
	 *
	 * @private
	 * @param {string} url The URL to download.
	 * @param {string} filename The output filename.
	 * @returns {Promise} A promise object.
	 */
	private download( url: string, filename: string ): Promise<void> {
		return new Promise( ( resolve ) => {
			get( url, ( response ) => {
				response.pipe( createWriteStream( this.getPath( filename ) ) );
				response.on( 'end', resolve );
			} );
		} );
	}

	/**
	 * Get path for file.
	 *
	 * @private
	 * @param {string} filename The output filename.
	 * @returns {string} The resolved full path of file.
	 */
	private getPath( filename: string ): string {
		return !isAbsolute( filename ) ? resolve( process.cwd(), filename ) : filename;
	}

	/**
	 * Returns styles for the diagram.
	 *
	 * @private
	 * @param {Flags} flags The current flags to use.
	 * @returns {Styles} An instance of the Styles class.
	 */
	private getStyles( flags: Flags ): Styles {
		const args: SkinParams = {
			direction: flags.direction,
			handwritten: flags.handwritten ? 'true' : 'false',
			colors: flags.colors,
			entityNamesOnly: flags['with-entity-names-only'],
			tableNamesOnly: flags['with-table-names-only'],
		};

		if ( flags.monochrome ) {
			return new MonochromeStyles( args );
		}

		if ( flags.format === Format.TXT ) {
			return new TextStyles( args );
		}

		return new Styles( args );
	}

}