package sbt

	import org.apache.ivy.{core, plugins, util}
	import core.module.id.ModuleRevisionId
	import core.module.descriptor.{DefaultArtifact, DefaultExtendsDescriptor, DefaultModuleDescriptor, ModuleDescriptor}
	import core.module.descriptor.{DefaultDependencyDescriptor, DependencyDescriptor}
	import plugins.parser.{m2, ModuleDescriptorParser, ModuleDescriptorParserRegistry, ParserSettings}
	import m2.{PomModuleDescriptorBuilder, PomModuleDescriptorParser}
	import plugins.repository.Resource
	import plugins.namespace.NamespaceTransformer
	import util.extendable.ExtendableItem

	import java.io.{File, InputStream}
	import java.net.URL
	import java.util.regex.Pattern

final class CustomPomParser(delegate: ModuleDescriptorParser, transform: (ModuleDescriptorParser, ModuleDescriptor) => ModuleDescriptor) extends ModuleDescriptorParser
{
	override def parseDescriptor(ivySettings: ParserSettings, descriptorURL: URL, validate: Boolean) =
		transform(this, delegate.parseDescriptor(ivySettings, descriptorURL, validate))
		
	override def parseDescriptor(ivySettings: ParserSettings, descriptorURL: URL, res: Resource, validate: Boolean) =
		transform(this, delegate.parseDescriptor(ivySettings, descriptorURL, res, validate))
    
	override def toIvyFile(is: InputStream, res: Resource, destFile: File, md: ModuleDescriptor) = delegate.toIvyFile(is, res, destFile, md)

	override def accept(res: Resource) = delegate.accept(res)
	override def getType() = delegate.getType()
	override def getMetadataArtifact(mrid: ModuleRevisionId, res: Resource) = delegate.getMetadataArtifact(mrid, res)
}
object CustomPomParser
{
	/** The key prefix that indicates that this is used only to store extra information and is not intended for dependency resolution.*/
	val InfoKeyPrefix = "info."
	val ApiURLKey = "info.apiURL"

	val SbtVersionKey = "sbtVersion"
	val ScalaVersionKey = "scalaVersion"
	val ExtraAttributesKey = "extraDependencyAttributes"
	private[this] val unqualifiedKeys = Set(SbtVersionKey, ScalaVersionKey, ExtraAttributesKey, ApiURLKey)

		// packagings that should be jars, but that Ivy doesn't handle as jars
	val JarPackagings = Set("eclipse-plugin", "hk2-jar", "orbit")
	val default = new CustomPomParser(PomModuleDescriptorParser.getInstance, defaultTransform)

	private[this] val TransformedHashKey = "e:sbtTransformHash"
	// A hash of the parameters transformation is based on.
	// If a descriptor has a different hash, we need to retransform it.
	private[this] val TransformHash: String = hash((unqualifiedKeys ++ JarPackagings).toSeq.sorted)
	private[this] def hash(ss: Seq[String]): String = Hash.toHex(Hash(ss.flatMap(_ getBytes "UTF-8").toArray))

		// Unfortunately, ModuleDescriptorParserRegistry is add-only and is a singleton instance.
	lazy val registerDefault: Unit = ModuleDescriptorParserRegistry.getInstance.addParser(default)

	def defaultTransform(parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor =
		if(transformedByThisVersion(md)) md else defaultTransformImpl(parser, md)

	private[this] def transformedByThisVersion(md: ModuleDescriptor): Boolean = 
	{
		val oldTransformedHashKey = "sbtTransformHash"
		val extraInfo = md.getExtraInfo
		// sbt 0.13.1 used "sbtTransformHash" instead of "e:sbtTransformHash" until #1192 so read both 
		Option(extraInfo).isDefined &&
		((Option(extraInfo get TransformedHashKey) orElse Option(extraInfo get oldTransformedHashKey)) match {
			case Some(TransformHash) => true
			case _ => false
		})
	}

	private[this] def defaultTransformImpl(parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor =
	{
		val properties = getPomProperties(md)

			// Extracts extra attributes (currently, sbt and Scala versions) stored in the <properties> element of the pom.
			// These are attached to the module itself.
		val filtered = shouldBeUnqualified(properties)

			// Extracts extra attributes for the dependencies.
			// Because the <dependency> tag in pom.xml cannot include additional metadata,
			//  sbt includes extra attributes in a 'extraDependencyAttributes' property.
			// This is read/written from/to a pure string (no element structure) because Ivy only
			//  parses the immediate text nodes of the property.
		val extraDepAttributes = getDependencyExtra(filtered)

			// Fixes up the detected extension in some cases missed by Ivy.
		val convertArtifacts = artifactExtIncorrect(md)

			// Merges artifact sections for duplicate dependency definitions
		val mergeDuplicates = IvySbt.hasDuplicateDependencies(md.getDependencies)

		val unqualify = toUnqualify(filtered)
		if(unqualify.isEmpty && extraDepAttributes.isEmpty && !convertArtifacts && !mergeDuplicates)
			md
		else
			addExtra(unqualify, extraDepAttributes, parser, md)
	}
		// The <properties> element of the pom is used to store additional metadata, such as for sbt plugins or for the base URL for API docs.
		// This is done because the pom XSD does not appear to allow extra metadata anywhere else.
		// The extra sbt plugin metadata in pom.xml does not need to be readable by maven, but the other information may be.
		// However, the pom.xml needs to be valid in all cases because other tools like repository managers may read the pom.xml.
	private[sbt] def getPomProperties(md: ModuleDescriptor): Map[String,String] =
	{
		import collection.JavaConverters._
		PomModuleDescriptorBuilder.extractPomProperties(md.getExtraInfo).asInstanceOf[java.util.Map[String,String]].asScala.toMap
	}
	private[sbt] def toUnqualify(propertyAttributes: Map[String, String]): Map[String, String] =
		(propertyAttributes - ExtraAttributesKey) map { case (k,v) => ("e:" + k, v) }

	private[this] def artifactExtIncorrect(md: ModuleDescriptor): Boolean =
		md.getConfigurations.exists(conf => md.getArtifacts(conf.getName).exists(art => JarPackagings(art.getExt)))
	private[this] def shouldBeUnqualified(m: Map[String, String]): Map[String, String] = m.filterKeys(unqualifiedKeys)
	
	private[this] def condAddExtra(properties: Map[String, String], id: ModuleRevisionId): ModuleRevisionId =
		if(properties.isEmpty) id else addExtra(properties, id)
	private[this] def addExtra(properties: Map[String, String], id: ModuleRevisionId): ModuleRevisionId =
	{
			import collection.JavaConverters._
		val oldExtra = qualifiedExtra(id)
		val newExtra = (oldExtra ++ properties).asJava
		ModuleRevisionId.newInstance(id.getOrganisation, id.getName, id.getBranch, id.getRevision, newExtra)
	}

	private[this] def getDependencyExtra(m: Map[String, String]): Map[ModuleRevisionId, Map[String,String]] =
		(m get ExtraAttributesKey) match {
			case None => Map.empty
			case Some(str) =>
				def processDep(m: ModuleRevisionId) = (simplify(m), filterCustomExtra(m, include=true))
				readDependencyExtra(str).map(processDep).toMap
		}

	def qualifiedExtra(item: ExtendableItem): Map[String,String] =
	{
			import collection.JavaConverters._			
		item.getQualifiedExtraAttributes.asInstanceOf[java.util.Map[String,String]].asScala.toMap
	}
	def filterCustomExtra(item: ExtendableItem, include: Boolean): Map[String,String] =
		(qualifiedExtra(item) filterKeys { k => qualifiedIsExtra(k) == include })

	def writeDependencyExtra(s: Seq[DependencyDescriptor]): Seq[String] =
		s.flatMap { dd =>
			val revId = dd.getDependencyRevisionId
			if(filterCustomExtra(revId, include=true).isEmpty)
				Nil
			else
				revId.encodeToString :: Nil
		}

		// parses the sequence of dependencies with extra attribute information, with one dependency per line
	def readDependencyExtra(s: String): Seq[ModuleRevisionId] =
		LinesP.split(s).map(_.trim).filter(!_.isEmpty).map(ModuleRevisionId.decode)

	private[this] val LinesP = Pattern.compile("(?m)^")

	def qualifiedIsExtra(k: String): Boolean = k.endsWith(ScalaVersionKey) || k.endsWith(SbtVersionKey)

		// Reduces the id to exclude custom extra attributes
		// This makes the id suitable as a key to associate a dependency parsed from a <dependency> element
		//  with the extra attributes from the <properties> section
	def simplify(id: ModuleRevisionId): ModuleRevisionId =
	{
			import collection.JavaConverters._			
		ModuleRevisionId.newInstance(id.getOrganisation, id.getName, id.getBranch, id.getRevision, filterCustomExtra(id, include=false).asJava)
	}

	private[this] def addExtra(dep: DependencyDescriptor, extra: Map[ModuleRevisionId, Map[String, String]]): DependencyDescriptor =
	{
		val extras = if(extra.isEmpty) None else extra get simplify(dep.getDependencyRevisionId)
		extras match {
			case None => dep
			case Some(extraAttrs) => transform(dep, revId => addExtra(extraAttrs, revId))
		}
	}
	private[this] def transform(dep: DependencyDescriptor, f: ModuleRevisionId => ModuleRevisionId): DependencyDescriptor =
		DefaultDependencyDescriptor.transformInstance(dep, namespaceTransformer(dep.getDependencyRevisionId, f), false)
	private[this] def extraTransformer(txId: ModuleRevisionId, extra: Map[String, String]): NamespaceTransformer =
		namespaceTransformer(txId, revId => addExtra(extra, revId) )

	private[this] def namespaceTransformer(txId: ModuleRevisionId, f: ModuleRevisionId => ModuleRevisionId): NamespaceTransformer =
		new NamespaceTransformer {
			def transform(revId: ModuleRevisionId): ModuleRevisionId  =  if(revId == txId) f(revId) else revId
			def isIdentity = false
		}

			import collection.JavaConverters._
	def addExtra(properties: Map[String, String], dependencyExtra: Map[ModuleRevisionId, Map[String,String]], parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor =
	{
		val dmd = new DefaultModuleDescriptor(parser, md.getResource)

		val mrid = addExtra(properties, md.getModuleRevisionId)
		val resolvedMrid = addExtra(properties, md.getResolvedModuleRevisionId)
		dmd.setModuleRevisionId(mrid)
		dmd.setResolvedModuleRevisionId(resolvedMrid)

		dmd.setDefault(md.isDefault)
		dmd.setHomePage(md.getHomePage)
		dmd.setDescription(md.getDescription)
		dmd.setLastModified(md.getLastModified)
		dmd.setStatus(md.getStatus())
		dmd.setPublicationDate(md.getPublicationDate())
		dmd.setResolvedPublicationDate(md.getResolvedPublicationDate())

		for(l <- md.getLicenses) dmd.addLicense(l)
		for( (key,value) <- md.getExtraInfo.asInstanceOf[java.util.Map[String,String]].asScala ) dmd.addExtraInfo(key, value)
		dmd.addExtraInfo(TransformedHashKey, TransformHash) // mark as transformed by this version, so we don't need to do it again
		for( (key, value) <- md.getExtraAttributesNamespaces.asInstanceOf[java.util.Map[String,String]].asScala ) dmd.addExtraAttributeNamespace(key, value)
		IvySbt.addExtraNamespace(dmd)

		val withExtra = md.getDependencies map { dd => addExtra(dd, dependencyExtra) }
		val unique = IvySbt.mergeDuplicateDefinitions(withExtra)
		unique foreach dmd.addDependency

		for( ed <- md.getInheritedDescriptors) dmd.addInheritedDescriptor( new DefaultExtendsDescriptor( md, ed.getLocation, ed.getExtendsTypes) )
		for( conf <- md.getConfigurations) {
			dmd.addConfiguration(conf)
			for(art <- md.getArtifacts(conf.getName)) {
				val ext = art.getExt
				val newExt = if( JarPackagings(ext) ) "jar" else ext
				val nart = new DefaultArtifact(mrid, art.getPublicationDate, art.getName, art.getType, newExt, art.getUrl, art.getQualifiedExtraAttributes)
				dmd.addArtifact(conf.getName, nart)
			}
		}
		dmd
	}
}