/// <summary>
 /// Transform minimum and maximum bounds from one space to another. For example,
 /// this is useful to find the minimum and maximum bounds of a tile collider
 /// within local space of tile system.
 /// </summary>
 /// <param name="bounds">Bounds that are to be transformed.</param>
 /// <param name="mat">Space-to-space transformation matrix.</param>
 /// <returns>
 /// The transformed bounds.
 /// </returns>
 private static BoxBounds SpaceToSpace(BoxBounds bounds, Matrix4x4 mat)
 {
     return(new BoxBounds(
                mat.MultiplyPoint3x4(bounds.Min),
                mat.MultiplyPoint3x4(bounds.Max)
                ));
 }
        public bool Encapsulate(BoxBounds other)
        {
            int matchCount = 0;

            for (int i = 0; i < 8 && matchCount != 4; ++i)
            {
                var p = this[i];
                for (int j = 0; j < 8; ++j)
                {
                    var q = other[j];
                    if (Approx(p, q, ErrorThreshold))
                    {
                        ++matchCount;
                        break;
                    }
                }
            }

            if (matchCount != 4)
            {
                return(false);
            }

            this.min = Vector3.Min(this.min, other.min);
            this.max = Vector3.Max(this.max, other.max);

            return(true);
        }
        /// <summary>
        /// Gather collider information from 3D <see cref="BoxCollider"/> component.
        /// </summary>
        /// <param name="info">Collider information instance.</param>
        /// <param name="collider">Component which resides somewhere within tile.</param>
        private void GatherInfo_BoxCollider3D(ColliderInfo info, BoxCollider collider)
        {
            var tileToSystemMatrix = this.worldToSystem * collider.transform.localToWorldMatrix;

            info.IsTrigger = collider.isTrigger;
            info.Material  = collider.sharedMaterial;
            info.Collider  = collider;
            info.Bounds    = SpaceToSpace(BoxBounds.FromBounds(collider.center, collider.size), tileToSystemMatrix);
        }
        /// <summary>
        /// Create new "Combined Collider" game object which covers bounds of tiles that
        /// have been reduced. Reduced tile game objects are stripped if they nolonger
        /// contain any useful components.
        /// </summary>
        /// <param name="info">Information about reduced collider.</param>
        /// <param name="reducedBounds">Bounding area of all reduced colliders.</param>
        private void Reduce(ColliderInfo info, BoxBounds reducedBounds)
        {
            var reducedGO = new GameObject("Combined Collider");

            // Can only copy layer and tag from collider information when the collider
            // information actually contains a game object!
            if (info.Tile.gameObject != null)
            {
                if (this.separateByLayer)
                {
                    reducedGO.layer = info.Tile.gameObject.layer;
                }
                if (this.separateByTag)
                {
                    reducedGO.tag = info.Tile.gameObject.tag;
                }
            }

            var reducedTransform = reducedGO.transform;

            reducedTransform.SetParent(this.system.transform, false);

            reducedBounds = SpaceToSpace(reducedBounds, reducedTransform.worldToLocalMatrix * this.systemToWorld);

            switch (info.Type)
            {
            case ColliderType.BoxCollider2D: {
                var collider = reducedGO.AddComponent <BoxCollider2D>();
                collider.offset         = reducedBounds.center;
                collider.size           = reducedBounds.size;
                collider.isTrigger      = info.IsTrigger;
                collider.sharedMaterial = info.Material as PhysicsMaterial2D;

                this.DestroyTrackedColliders();
            }
            break;

            case ColliderType.BoxCollider3D: {
                var collider = reducedGO.AddComponent <BoxCollider>();
                collider.center         = reducedBounds.center;
                collider.size           = reducedBounds.size;
                collider.isTrigger      = info.IsTrigger;
                collider.sharedMaterial = info.Material as PhysicMaterial;

                this.DestroyTrackedColliders();
            }
            break;

            default:
                Debug.LogWarning(string.Format("Collider reduction not implemented for '{0}'.", info.Type));
                break;
            }
        }
        /// <summary>
        /// Gather hypothetical collider information from solid flag.
        /// </summary>
        /// <param name="info">Collider information instance.</param>
        private void GatherInfo_SolidFlag(ColliderInfo info)
        {
            info.IsTrigger = false;

            // Bounds of 2D colliders need to be consistent.
            Vector3 boundsSize = this.system.CellSize;

            if (info.Type == ColliderType.BoxCollider2D)
            {
                boundsSize.z = 1f;
            }

            info.Bounds = BoxBounds.FromBounds(this.system.LocalPositionFromTileIndex(info.Row, info.Column), boundsSize);
        }
        /// <summary>
        /// Gather collider information from <see cref="BoxCollider2D"/> component.
        /// </summary>
        /// <param name="info">Collider information instance.</param>
        /// <param name="collider">Component which resides somewhere within tile.</param>
        private void GatherInfo_BoxCollider2D(ColliderInfo info, BoxCollider2D collider)
        {
            var tileToSystemMatrix = this.worldToSystem * collider.transform.localToWorldMatrix;

            // Bounds of 2D colliders need to be consistent.
            Vector3 boundsSize = collider.size;

            boundsSize.z = 1f;

            Vector3 center = collider.offset;

            info.IsTrigger = collider.isTrigger;
            info.Material  = collider.sharedMaterial;
            info.Collider  = collider;
            info.Bounds    = SpaceToSpace(BoxBounds.FromBounds(center, boundsSize), tileToSystemMatrix);
        }
        /// <summary>
        /// Scans for and applies reductions if specified anchor tile contains a collider.
        /// </summary>
        /// <remarks>
        /// <para>This method searches from left-to-right from specified anchor tile to
        /// find furthest tile horizontally that it can be reduced with. Once the initial
        /// horizontal sequence is known this method proceeds to scan vertically for other
        /// sequences of colliders that can further reduce the initial horizontal sequence.</para>
        /// </remarks>
        /// <param name="anchor">Index of anchor tile.</param>
        /// <returns></returns>
        private int Scan(TileIndex anchor)
        {
            ColliderInfo anchorInfo = null;

            this.GetColliderInfo(ref anchorInfo, anchor);
            if (anchorInfo == null)
            {
                return(anchor.column);
            }

            ColliderInfo info = null, firstInfo = null, lastInfo = null;

            // Begin tracking list of collider components which will be combined.
            this.ResetReducedColliderTracking();
            this.TrackReducedCollider(anchorInfo.Row, anchorInfo.Column, anchorInfo.Collider);

            try {
                TileIndex target        = anchor;
                BoxBounds reducedBounds = anchorInfo.Bounds;

                // Scan rightwards for colliders which can be combined with anchor.
                for (int column = anchor.column + 1; column < this.system.ColumnCount; ++column)
                {
                    if (this.markedTiles[anchor.row, column])
                    {
                        break;
                    }

                    this.GetColliderInfo(ref info, anchor.row, column);
                    if (!this.CanReduce(info, anchorInfo))
                    {
                        break;
                    }

                    // Can candidate collider be combined with anchor?
                    if (!reducedBounds.Encapsulate(info.Bounds))
                    {
                        break;
                    }

                    target.column = column;

                    this.TrackReducedCollider(info.Row, info.Column, info.Collider);
                }

                this.GetColliderInfo(ref info, target);
                var prevFirstBounds = anchorInfo.Bounds;
                var prevLastBounds  = info.Bounds;

                // Extend downwards if possible, though avoid crossing T-junctions since splitting
                // a T-junction will often cause more colliders to occur in result.
                for (int row = anchor.row + 1; row < this.system.RowCount; ++row)
                {
                    this.GetColliderInfo(ref firstInfo, row, anchor.column);
                    this.GetColliderInfo(ref lastInfo, row, target.column);
                    if (firstInfo == null || lastInfo == null || !this.CanReduce(firstInfo, anchorInfo) || this.CheckForTJunction(firstInfo, lastInfo))
                    {
                        goto FinishedExtendingDownards;
                    }

                    // Does first and last tile from previous row extend downwards?
                    if (!prevFirstBounds.Encapsulate(firstInfo.Bounds) || !prevLastBounds.Encapsulate(lastInfo.Bounds))
                    {
                        goto FinishedExtendingDownards;
                    }

                    // No need to track collider from `lastInfo` since it will be included
                    // within the following loop.
                    this.candidateColliders.Clear();
                    this.candidateColliders.Add(firstInfo.Collider);

                    // Reduce bounds for strip.
                    BoxBounds stripBounds = firstInfo.Bounds;
                    for (int column = anchor.column + 1; column <= target.column; ++column)
                    {
                        this.GetColliderInfo(ref info, row, column);
                        if (!this.CanReduce(info, anchorInfo) || !stripBounds.Encapsulate(info.Bounds))
                        {
                            goto FinishedExtendingDownards;
                        }
                        this.candidateColliders.Add(info.Collider);
                    }

                    // The following should not fail, but let's just be safe!
                    if (!reducedBounds.Encapsulate(stripBounds))
                    {
                        Debug.LogWarning("Unable to encapsulate collider on reduce.");
                        goto FinishedExtendingDownards;
                    }

                    target.row = row;

                    // Accept and track candidate colliders!
                    for (int i = 0; i < this.candidateColliders.Count; ++i)
                    {
                        this.TrackReducedCollider(row, anchor.column + i, this.candidateColliders[i]);
                    }

                    prevFirstBounds = firstInfo.Bounds;
                    prevLastBounds  = lastInfo.Bounds;
                }

FinishedExtendingDownards:
                ;

                bool hasManyColliders           = (this.reducedColliderCount > 1);
                bool hasOneHypotheticalCollider = (this.reducedColliderCount == 1 && this.reducedColliderComponents.Count == 0);
                if (hasManyColliders || hasOneHypotheticalCollider)
                {
                    this.Reduce(anchorInfo, reducedBounds);
                }

                return(target.column);
            }
            finally {
                ColliderInfo.Despawn(anchorInfo);
                ColliderInfo.Despawn(info);
                ColliderInfo.Despawn(firstInfo);
                ColliderInfo.Despawn(lastInfo);
            }
        }