namespace ImageResources {

    public class ImageSheet {

        // essentially this amounts to the tile count (as of now)
        // For aggregated image sheets, this represents a relative weight of how much "stuff"
        // this sheet loads so that the overall percentage can be shown as a function of all
        // sheets more accurately.
        internal int _weight = 1;

        public constructor() { }

        public static ImageSheet loadFromResources(object name) {
            return ImageSheet._initImpl(name, false, true);
        }

        static ImageSheet loadFromResourcesAsync(object name) {
            return ImageSheet._initImpl(name, true, true);
        }

        // TODO: abstract classes
        public ImageResource getImage(string resourcePath) { throw new Exception(); }
        public bool isDone() { throw new Exception(); }
        public float getProgress() { throw new Exception(); }
        public List<string> getFiles() { throw new Exception(); }

        private static ImageSheet _initImpl(object name, bool isAsync, bool allowLists) {

            // If name is actually a LIST of names, then the user actually wants an
            // aggregated image sheet.
            if (allowLists && Core.typeof(name) == Core.Type.LIST) {
                List<BasicImageSheet> sheets = new List<BasicImageSheet>();

                // TODO: this is a List<object> when coming from a Crayon scope and a
                // List<string> when coming from an Acrylic scope. Because Acrylic isn't
                // released yet, this is set to List<object> for now, but eventually I
                // really need to fix the generics story in Acrylic.
                for (object t : (List<object>) name) {
                    sheets.add((BasicImageSheet) ImageSheet._initImpl(t, isAsync, false));
                }
                return new AggregatedImageSheet(sheets);
            }

            // asserts on not found, so info is always non-null/valid
            ManifestSheetInfo info = ImageResourceManifest.getSheetInfo((string) name);

            return new BasicImageSheet(info, isAsync);
        }

    }

    internal class BasicImageSheet : ImageSheet {

        private bool _isLoaded = false;
        private ManifestSheetInfo _info;
        private ImageSheetLoaderAsync _loader;
        private string _name;
        internal string _id;

        // List where the chunk ID is the index and the value is an ImageResource instance.
        private List<ImageResource> _chunks;

        public constructor(ManifestSheetInfo info, bool isAsync) : base() {
            this._info = info;
            this._name = info.name;
            this._id = info.id;

            Dictionary<int, ManifestChunkInfo> chunks = info.chunks;
            List<ManifestTileInfo> tiles = new List<ManifestTileInfo>();
            for (ManifestChunkInfo chunk : chunks.values()) {
                tiles.concat(chunk.tiles);
            }

            this._weight = tiles.length;

            if (isAsync) {
                this._loader = new ImageSheetLoaderAsync(tiles).load();
            } else {
                Dictionary<string, ImageResource> lookup = new Dictionary<string, ImageResource>();
                for (ManifestTileInfo tile : tiles) {
                    string genName = tile.genFileName;
                    int width = tile.width;
                    int height = tile.height;
                    lookup[genName] = (ImageResource) ImageLoader._loadImpl(
                        genName,
                        true, // is generated name
                        false, // is asynchronous
                        width,
                        height);
                }
                this._turnTilesIntoSheet(lookup);
            }
        }

        public override ImageResource getImage(string resourcePath) {
            if (!this._isLoaded) throw new ImageResources.LoadFailedException("Cannot load image from sheet that is not finished loading.");

            // Convert the _loader object into a finalized sheet and GC the loader
            // if present.
            if (this._loader != null) {
                this._turnTilesIntoSheet(this._loader.completed);
                this._loader = null;
            }

            ManifestFileInfo res = ImageResourceManifest.getFileInfo(resourcePath, true);
            if (res.sheetId != this._id) {
                throw new ImageResources.ImageNotFoundException("Image sheet '" + this._name + "' does not contain resource: '{0}'", resourcePath);
            }

            ImageResource img = this._chunks[res.chunkId];

            ImageResource output = new ImageResource(res.width, res.height, false);
            output._nativeResource = img._nativeResource;
            output._x = res.x;
            output._y = res.y;
            return output;
        }

        private static List<object> _reusableStatusOut = new List<object>();

        public override bool isDone() {
            if (this._isLoaded) return true;
            BasicImageSheet._reusableStatusOut.clear();
            this._loader.getStatus(BasicImageSheet._reusableStatusOut);
            if ((bool) BasicImageSheet._reusableStatusOut[0]) {
                this._isLoaded = true;
            }
            return this._isLoaded;
        }

        public override float getProgress() {
            if (this._isLoaded) return 1.0;
            BasicImageSheet._reusableStatusOut.clear();
            this._loader.getStatus(BasicImageSheet._reusableStatusOut);
            if ((bool) BasicImageSheet._reusableStatusOut[0]) {
                this._isLoaded = true;
                return 1.0;
            }
            return (float) BasicImageSheet._reusableStatusOut[1];
        }

        private void _turnTilesIntoSheet(Dictionary<string, ImageResource> tilesByName) {
            Dictionary<int, ManifestChunkInfo> chunkMetadata = this._info.chunks;
            List<ImageResource> chunks = new List<ImageResource>();
            for (int chunkId : chunkMetadata.keys()) {
                ManifestChunkInfo chunk = chunkMetadata[chunkId];
                ImageResource img = new ImageResource(chunk.width, chunk.height);
                NativeImageData res = img._nativeResource;
                res.lock();
                for (ManifestTileInfo tile : chunk.tiles) {
                    string name = tile.genFileName;
                    int x = tile.x;
                    int y = tile.y;
                    res.blit(tilesByName[name], x, y);
                }
                res.unlock();
                while (chunks.length <= chunkId) { chunks.add(null); }
                chunks[chunkId] = img;
            }
            this._chunks = chunks;
            this._isLoaded = true;
            this._info = null; // no longer needed. feed to the GC.
        }

        public override List<string> getFiles() {
            return ImageResourceManifest.filesBySheetId[this._id].clone();
        }
    }

    internal class ImageSheetLoaderAsync {
        internal List<ManifestTileInfo> tiles;
        internal Dictionary<string, ImageResource> completed = new Dictionary<string, ImageResource>();
        internal Dictionary<string, ImageLoader> inProgress = new Dictionary<string, ImageLoader>();

        constructor(List<ManifestTileInfo> tiles) {
            this.tiles = tiles;
        }

        public ImageSheetLoaderAsync load() {
            for (ManifestTileInfo tile : this.tiles) {
                string genName = tile.genFileName;
                int width = tile.width;
                int height = tile.height;
                this.inProgress[genName] = (ImageLoader) ImageLoader._loadImpl(
                    genName,
                    true, // is generated filename
                    true, // is asynchronous
                    width,
                    height);
            }
            return this;
        }

        public void getStatus(List<object> statusOut) {
            if (this.inProgress.length > 0) {
                for (string key : this.inProgress.keys()) {
                    if (this.inProgress[key].isDone()) {
                        this.completed[key] = this.inProgress[key].getImage();
                        this.inProgress.remove(key);
                    }
                }
            }

            if (this.inProgress.length == 0) {
                statusOut.add(true);
                statusOut.add(1.0);
            } else {
                statusOut.add(false);
                statusOut.add(1.0 * this.completed.length / this.tiles.length);
            }
        }
    }
}
