package sbt

	import Types.{const, idFun}
	import Def.Initialize
	import java.net.URI
	import ScopeFilter.Data

object ScopeFilter
{
	type ScopeFilter = Base[Scope]
	type AxisFilter[T] = Base[ScopeAxis[T]]
	type ProjectFilter = AxisFilter[Reference]
	type ConfigurationFilter = AxisFilter[ConfigKey]
	type TaskFilter = AxisFilter[AttributeKey[_]]

	/** Constructs a Scope filter from filters for the individual axes.
	* If a project filter is not supplied, the enclosing project is selected.
	* If a configuration filter is not supplied, global is selected.
	* If a task filter is not supplied, global is selected.
	* Generally, always specify the project axis.*/
	def apply(projects: ProjectFilter = inProjects(ThisProject), configurations: ConfigurationFilter = globalAxis, tasks: TaskFilter = globalAxis): ScopeFilter =
		new ScopeFilter {
			private[sbt] def apply(data: Data): Scope => Boolean =
			{
				val pf = projects(data)
				val cf = configurations(data)
				val tf = tasks(data)
				s => pf(s.project) && cf(s.config) && tf(s.task)
			}
		}

	def debug(delegate: ScopeFilter): ScopeFilter =
		new ScopeFilter {
			private[sbt] def apply(data: Data): Scope => Boolean =
			{
				val d = delegate(data)
				scope => {
					val accept = d(scope)
					println( (if(accept) "ACCEPT " else "reject ") + scope)
					accept
				}
			}
		}

	final class SettingKeyAll[T] private[sbt](i: Initialize[T]) {
		/** Evaluates the initialization in all scopes selected by the filter.  These are dynamic dependencies, so
		* static inspections will not show them. */
		def all(sfilter: => ScopeFilter): Initialize[Seq[T]] = Def.bind(getData) { data =>
			data.allScopes.toSeq.filter(sfilter(data)).map(s => Project.inScope(s, i)).join
		}
	}
	final class TaskKeyAll[T] private[sbt](i: Initialize[Task[T]]) {
		/** Evaluates the task in all scopes selected by the filter.  These are dynamic dependencies, so
		* static inspections will not show them. */
		def all(sfilter: => ScopeFilter): Initialize[Task[Seq[T]]] = Def.bind(getData) { data =>
				import std.TaskExtra._
			data.allScopes.toSeq.filter(sfilter(data)).map(s => Project.inScope(s, i)).join(_.join)
		}
	}

	private[sbt] val Make = new Make {}
	trait Make
	{
		/** Selects the Scopes used in `<key>.all(<ScopeFilter>)`.*/
		type ScopeFilter = Base[Scope]

		/** Selects Scopes with a global task axis. */
		def inGlobalTask: TaskFilter = globalAxis[AttributeKey[_]]
		/** Selects Scopes with a global project axis. */
		def inGlobalProject: ProjectFilter = globalAxis[Reference]
		/** Selects Scopes with a global configuration axis. */
		def inGlobalConfiguration: ConfigurationFilter = globalAxis[ConfigKey]

		/** Selects all scopes that apply to a single project.  Global and build-level scopes are excluded. */
		def inAnyProject: ProjectFilter = selectAxis(const { case p: ProjectRef => true; case _ => false })
		/** Accepts all values for the task axis except Global. */
		def inAnyTask: TaskFilter = selectAny[AttributeKey[_]]
		/** Accepts all values for the configuration axis except Global. */
		def inAnyConfiguration: ConfigurationFilter = selectAny[ConfigKey]

		/** Selects Scopes that have a project axis that is aggregated by `ref`, transitively if `transitive` is true.
		* If `includeRoot` is true, Scopes with `ref` itself as the project axis value are also selected. */
		def inAggregates(ref: ProjectReference, transitive: Boolean=true, includeRoot: Boolean=true): ProjectFilter =
			byDeps(ref, transitive=transitive, includeRoot=includeRoot, aggregate=true, classpath=false)

		/** Selects Scopes that have a project axis that is a dependency of `ref`, transitively if `transitive` is true.
		* If `includeRoot` is true, Scopes with `ref` itself as the project axis value are also selected. */
		def inDependencies(ref: ProjectReference, transitive: Boolean=true, includeRoot: Boolean=true): ProjectFilter =
			byDeps(ref, transitive=transitive, includeRoot=includeRoot, aggregate=false, classpath=true)
		
		/** Selects Scopes that have a project axis with one of the provided values.*/
		def inProjects(projects: ProjectReference*): ProjectFilter = ScopeFilter.inProjects(projects : _*)

		/** Selects Scopes that have a task axis with one of the provided values.*/		
		def inTasks(tasks: Scoped*): TaskFilter = {
			val ts = tasks.map(_.key).toSet
			selectAxis[AttributeKey[_]](const(ts))
		}

		/** Selects Scopes that have a task axis with one of the provided values.*/
		def inConfigurations(configs: Configuration*): ConfigurationFilter = {
			val cs = configs.map(_.name).toSet
			selectAxis[ConfigKey](const(c => cs(c.name)))
		}

		implicit def settingKeyAll[T](key: Initialize[T]): SettingKeyAll[T] = new SettingKeyAll[T](key)
		implicit def taskKeyAll[T](key: Initialize[Task[T]]): TaskKeyAll[T] = new TaskKeyAll[T](key)
	}

	/** Information provided to Scope filters.  These provide project relationships,
	* project reference resolution, and the list of all static Scopes.*/
	private final class Data(val units: Map[URI, LoadedBuildUnit], val resolve: ProjectReference => ProjectRef, val allScopes: Set[Scope])

	/** Constructs a Data instance from the list of static scopes and the project relationships.*/
	private[this] val getData: Initialize[Data] =
		Def.setting {
			val build = Keys.loadedBuild.value
			val scopes = Def.StaticScopes.value
			val thisRef = Keys.thisProjectRef.?.value
			val current = thisRef match {
				case Some(ProjectRef(uri, _)) => uri
				case None => build.root
			}
			val rootProject = Load.getRootProject(build.units)
			val resolve: ProjectReference => ProjectRef = p => (p, thisRef) match {
				case (ThisProject, Some(pref)) => pref
				case _ => Scope.resolveProjectRef(current, rootProject, p)
			}
			new Data(build.units, resolve, scopes)
		}

	private[this] def getDependencies(structure: Map[URI,LoadedBuildUnit], classpath: Boolean, aggregate: Boolean): ProjectRef => Seq[ProjectRef] =
		ref => Project.getProject(ref, structure).toList flatMap { p =>
			(if(classpath) p.dependencies.map(_.project) else Nil) ++
			(if(aggregate) p.aggregate else Nil)
		}

	private[this] def byDeps(ref: ProjectReference, transitive: Boolean, includeRoot: Boolean, aggregate: Boolean, classpath: Boolean): ProjectFilter =
		inResolvedProjects { data =>
			val resolvedRef = data.resolve(ref)
			val direct = getDependencies(data.units, classpath=classpath, aggregate=aggregate)
			if(transitive) {
				val full = Dag.topologicalSort(resolvedRef)(direct)
				if(includeRoot) full else full dropRight 1
			} else {
				val directDeps = direct(resolvedRef)
				if(includeRoot) resolvedRef +: directDeps else directDeps
			}
		}

	private def inProjects(projects: ProjectReference*): ProjectFilter =
		inResolvedProjects( data => projects.map(data.resolve) )

	private[this] def inResolvedProjects(projects: Data => Seq[ProjectRef]): ProjectFilter =
		selectAxis(data => projects(data).toSet)

	private[this] def globalAxis[T]: AxisFilter[T] = new AxisFilter[T] {
		private[sbt] def apply(data: Data): ScopeAxis[T] => Boolean =
			_ == Global
	}
	private[this] def selectAny[T]: AxisFilter[T] = selectAxis(const(const(true)))
	private[this] def selectAxis[T](f: Data => T => Boolean): AxisFilter[T] = new AxisFilter[T] {
		private[sbt] def apply(data: Data): ScopeAxis[T] => Boolean = {
			val g = f(data)
			s => s match {
				case Select(t) => g(t)
				case _ => false
			}
		}
	}

	/** Base functionality for filters on values of type `In` that need access to build data.*/
	sealed abstract class Base[In]
	{ self =>
		/** Implements this filter. */
		private[sbt] def apply(data: Data): In => Boolean
	
		/** Constructs a filter that selects values that match this filter but not `other`.*/
		def --(other: Base[In]): Base[In] = this && -other

		/** Constructs a filter that selects values that match this filter and `other`.*/
		def &&(other: Base[In]): Base[In] = new Base[In] {
			private[sbt] def apply(data: Data): In => Boolean = {
				val a = self(data)
				val b = other(data)
				s => a(s) && b(s)
			}
		}

		/** Constructs a filter that selects values that match this filter or `other`.*/
		def ||(other: Base[In]): Base[In] = new Base[In] {
			private[sbt] def apply(data: Data): In => Boolean = {
				val a = self(data)
				val b = other(data)
				s => a(s) || b(s)
			}
		}
		/** Constructs a filter that selects values that do not match this filter.*/
		def unary_- : Base[In] = new Base[In] {
			private[sbt] def apply(data: Data): In => Boolean = {
				val a = self(data)
				s => !a(s)
			}
		}
	}
	
}