<?php

namespace Composer\Installers;

use Composer\IO\IOInterface;
use Composer\Composer;
use Composer\Package\PackageInterface;

abstract class BaseInstaller {

	/** @var array<string, string> */
	protected $locations = array();
	/** @var Composer */
	protected $composer;
	/** @var PackageInterface */
	protected $package;
	/** @var IOInterface */
	protected $io;

	/**
	 * Initializes base installer.
	 */
	public function __construct( PackageInterface $package, Composer $composer, IOInterface $io ) {
		$this->composer = $composer;
		$this->package  = $package;
		$this->io       = $io;
	}

	/**
	 * Return the install path based on package type.
	 */
	public function getInstallPath( PackageInterface $package, string $frameworkType = '' ): string {
		$type = $this->package->getType();

		$prettyName = $this->package->getPrettyName();
		if ( strpos( $prettyName, '/' ) !== false ) {
			list($vendor, $name) = explode( '/', $prettyName );
		} else {
			$vendor = '';
			$name   = $prettyName;
		}

		$availableVars = $this->inflectPackageVars( compact( 'name', 'vendor', 'type' ) );

		$extra = $package->getExtra();
		if ( ! empty( $extra['installer-name'] ) ) {
			$availableVars['name'] = $extra['installer-name'];
		}

		$extra = $this->composer->getPackage()->getExtra();
		if ( ! empty( $extra['installer-paths'] ) ) {
			$customPath = $this->mapCustomInstallPaths( $extra['installer-paths'], $prettyName, $type, $vendor );
			if ( $customPath !== false ) {
				return $this->templatePath( $customPath, $availableVars );
			}
		}

		$packageType = substr( $type, strlen( $frameworkType ) + 1 );
		$locations   = $this->getLocations( $frameworkType );
		if ( ! isset( $locations[ $packageType ] ) ) {
			throw new \InvalidArgumentException( sprintf( 'Package type "%s" is not supported', $type ) );
		}

		return $this->templatePath( $locations[ $packageType ], $availableVars );
	}

	/**
	 * For an installer to override to modify the vars per installer.
	 *
	 * @param  array<string, string> $vars This will normally receive array{name: string, vendor: string, type: string}
	 * @return array<string, string>
	 */
	public function inflectPackageVars( array $vars ): array {
		return $vars;
	}

	/**
	 * Gets the installer's locations
	 *
	 * @return array<string, string> map of package types => install path
	 */
	public function getLocations( string $frameworkType ) {
		return $this->locations;
	}

	/**
	 * Replace vars in a path
	 *
	 * @param  array<string, string> $vars
	 */
	protected function templatePath( string $path, array $vars = array() ): string {
		if ( strpos( $path, '{' ) !== false ) {
			extract( $vars );
			preg_match_all( '@\{\$([A-Za-z0-9_]*)\}@i', $path, $matches );
			if ( ! empty( $matches[1] ) ) {
				foreach ( $matches[1] as $var ) {
					$path = str_replace( '{$' . $var . '}', $$var, $path );
				}
			}
		}

		return $path;
	}

	/**
	 * Search through a passed paths array for a custom install path.
	 *
	 * @param  array<string, string[]|string> $paths
	 * @return string|false
	 */
	protected function mapCustomInstallPaths( array $paths, string $name, string $type, ?string $vendor = null ) {
		foreach ( $paths as $path => $names ) {
			$names = (array) $names;
			if ( in_array( $name, $names ) || in_array( 'type:' . $type, $names ) || in_array( 'vendor:' . $vendor, $names ) ) {
				return $path;
			}
		}

		return false;
	}

	protected function pregReplace( string $pattern, string $replacement, string $subject ): string {
		$result = preg_replace( $pattern, $replacement, $subject );
		if ( null === $result ) {
			throw new \RuntimeException( 'Failed to run preg_replace with ' . $pattern . ': ' . preg_last_error() );
		}

		return $result;
	}
}
