// -------------------------
        // Autostep methods
        // -------------------------
        // flash the good and the bad tiles
        public void FlashBeforeAutostep()
        {
            // restore the puzzle state to the last good state
            SaveAll();
            RestoreAll();
            Vector2Int lastGood, nextGood;

            CheckPuzzleStateComplete(false, out lastGood, out nextGood);
            List <TileFlashArgs> flashList = new List <TileFlashArgs>();
            // flash a tile to from to with the bad color
            TileStatus badTile = tileNeighbours[nextGood.x, nextGood.y];

            flashList.Add(new TileFlashArgs
            {
                id   = new Vector2Int(badTile.id.x, badTile.id.y),
                type = FlashType.Bad
            });
            // flash a tile to go to with the good color
            TileStatus goodTile = tileNeighbours[badTile.id.x, badTile.id.y];

            flashList.Add(new TileFlashArgs
            {
                id   = new Vector2Int(goodTile.id.x, goodTile.id.y),
                type = FlashType.Good
            });
            GlobalManager.MInstantMessage.DeliverMessage(InstantMessageType.PuzzleFlashTile, this, flashList);
        }
        // ---------------------
        // logic checking
        // ---------------------
        // this method performs several calculations at once:
        // 1. it checks if the puzzle is assembled, and returns true/false according to the state of completeness
        // 2. it looks for the latest tile which is already in its place, and returns its position in lastGood
        // 3. it looks for the tile which should be placed after the lastGood one, and returns its position in nextGood
        // 4. it alse flashes the already assembled tiles if needed
        bool CheckPuzzleStateComplete(bool flashTiles, out Vector2Int lastGood, out Vector2Int nextGood)
        {
            Vector2Int current = new Vector2Int(0, 0);
            Vector2Int last    = new Vector2Int(-1, -1);
            Vector2Int next    = new Vector2Int(0, 0);

            nextGood = next;
            bool isComplete = true;
            List <TileFlashArgs> flashList = new List <TileFlashArgs>();

            for (int y = 0; y < descriptor.init.height; y++)
            {
                for (int x = 0; x < descriptor.init.width; x++)
                {
                    TileStatus tileStatus = tileNeighbours[x, y];
                    if (tileStatus.id != current || tileStatus.angle != 0)
                    {
                        isComplete = false;
                        // set the nextGood position
                        if (tileStatus.id.x == next.x && tileStatus.id.y == next.y)
                        {
                            nextGood = new Vector2Int {
                                x = x, y = y
                            };
                        }
                    }
                    else
                    {
                        if (isComplete)
                        {
                            if (flashTiles)
                            {
                                // this tile is in the place, so it should flash
                                flashList.Add(new TileFlashArgs
                                {
                                    id   = new Vector2Int(x, y),
                                    type = FlashType.Good
                                });
                            }
                            last = current;
                            // calculate the nextGood parameters
                            if (++next.x >= descriptor.init.width)
                            {
                                next.x = 0;
                                next.y++;
                            }
                        }
                    }
                    current.x++;
                }
                current.x = 0;
                current.y++;
            }
            lastGood = last;
            if (flashTiles)
            {
                GlobalManager.MInstantMessage.DeliverMessage(InstantMessageType.PuzzleFlashTile, this, flashList);
            }
            return(isComplete);
        }
        // this method checks if the current puzzle state is "better" than the one in parameters
        // this means that the current puzzle state has more first "inplace" tiles than the other
        // if current puzzle state is better than aState, then return 1
        // if current puzzle state is worse than aState, then return -1
        // otherwise, return 0
        int IsPuzzleStateBetter(string aState, out Vector2Int thisGoodTile, out Vector2Int otherGoodTile)
        {
            thisGoodTile  = new Vector2Int(-1, -1);
            otherGoodTile = new Vector2Int(-1, -1);
            if (string.IsNullOrEmpty(aState))
            {
                return(1);
            }
            TileStatus[,] otherTiles = new TileStatus[descriptor.init.width, descriptor.init.height];
            ParseStatusString(aState, ref otherTiles);
            bool checkThis  = true;
            bool checkOther = true;

            for (int y = 0; y < descriptor.init.height && (checkThis || checkOther); y++)
            {
                for (int x = 0; x < descriptor.init.width && (checkThis || checkOther); x++)
                {
                    if (checkThis)
                    {
                        TileStatus thisTile = tileNeighbours[x, y];
                        if (thisTile.id.x == x && thisTile.id.y == y && thisTile.angle == 0)
                        {
                            thisGoodTile.x = x;
                            thisGoodTile.y = y;
                        }
                        else
                        {
                            checkThis = false;
                        }
                    }
                    if (checkOther)
                    {
                        TileStatus otherTile = otherTiles[x, y];
                        if (otherTile.id.x == x && otherTile.id.y == y && otherTile.angle == 0)
                        {
                            otherGoodTile.x = x;
                            otherGoodTile.y = y;
                        }
                        else
                        {
                            checkOther = false;
                        }
                    }
                }
            }
            int thisId  = thisGoodTile.y * descriptor.init.width + thisGoodTile.x;
            int otherId = otherGoodTile.y * descriptor.init.width + otherGoodTile.x;

            return(thisId > otherId ? 1 : (thisId > otherId ? -1 : 0));
        }
        IEnumerator PerformAutoStep()
        {
            // wait for a frame to ensure a clean steady puzzle state
            yield return(null);

            //Debug.Log("Starting autostep");
            Vector2Int lastGood, nextGood;

            CheckPuzzleStateComplete(false, out lastGood, out nextGood);
            TileStatus tileStatus = tileNeighbours[nextGood.x, nextGood.y];

            byte[] solution = AutoStepSolutions.GetSolution(
                descriptor.init.height,
                descriptor.init.width,
                tileStatus.id.y * descriptor.init.width + tileStatus.id.x,
                nextGood.y,
                nextGood.x,
                tileStatus.angle
                );
            if (solution != null)
            {
                //Debug.Log("Running autostep for " + nextGood.ToString() + " to " + lastGood.ToString());
                descriptor.state.AutocompleteUsed = true;
                PuzzleButtonController.PuzzleButtonArgs buttonArgs = new PuzzleButtonController.PuzzleButtonArgs
                {
                    id   = new Vector2Int(0, 0),
                    fast = 0.5f
                };
                for (int i = 0; i < solution.Length && !puzzleComplete; i++)
                {
                    buttonArgs.id.y = (solution[i] >> 4) & 0xf;
                    buttonArgs.id.x = solution[i] & 0xf;
                    //Debug.Log(">> Step " + i.ToString() + ": rotating button " + buttonArgs.id.ToString());
                    RotateButton(buttonArgs, false);
                    while (!buttonRotated)
                    {
                        yield return(null);
                    }
                }
                //Debug.Log("Saving state");
                SaveAll();
                // tight vibe sound
                autostepJingle.pitchFactor = autostepPitchRange.Random;
                GlobalManager.MAudio.PlaySFX(autostepJingle);
                //Debug.Log("Notifying of autostep");
            }
            GlobalManager.MInstantMessage.DeliverMessage(InstantMessageType.PuzzleAutostepUsed, this);
        }
        // this method creates a status string out from the current status of tiles
        string BuildStatusString()
        {
            string statusString = "";

            for (int y = 0; y < descriptor.init.height; y++)
            {
                for (int x = 0; x < descriptor.init.width; x++)
                {
                    if (statusString != "")
                    {
                        statusString += ".";
                    }
                    TileStatus tileStatus = tileNeighbours[x, y];
                    statusString += tileStatus.id.x.ToString() + "," + tileStatus.id.y.ToString() + "," + tileStatus.angle.ToString();
                }
            }
            return(statusString);
        }
        // button rotation
        void RotateTilesWith(Vector2Int buttonId, bool saveAfterRotation = true)
        {
            // update tiles
            TileStatus temp = new TileStatus
            {
                id = new Vector2Int
                {
                    x = tileNeighbours[buttonId.x, buttonId.y].id.x,
                    y = tileNeighbours[buttonId.x, buttonId.y].id.y
                },
                angle = tileNeighbours[buttonId.x, buttonId.y].angle
            };

            tileNeighbours[buttonId.x, buttonId.y].id.x  = tileNeighbours[buttonId.x, buttonId.y + 1].id.x;
            tileNeighbours[buttonId.x, buttonId.y].id.y  = tileNeighbours[buttonId.x, buttonId.y + 1].id.y;
            tileNeighbours[buttonId.x, buttonId.y].angle = (tileNeighbours[buttonId.x, buttonId.y + 1].angle + 1) % 4;

            tileNeighbours[buttonId.x, buttonId.y + 1].id.x  = tileNeighbours[buttonId.x + 1, buttonId.y + 1].id.x;
            tileNeighbours[buttonId.x, buttonId.y + 1].id.y  = tileNeighbours[buttonId.x + 1, buttonId.y + 1].id.y;
            tileNeighbours[buttonId.x, buttonId.y + 1].angle = (tileNeighbours[buttonId.x + 1, buttonId.y + 1].angle + 1) % 4;

            tileNeighbours[buttonId.x + 1, buttonId.y + 1].id.x  = tileNeighbours[buttonId.x + 1, buttonId.y].id.x;
            tileNeighbours[buttonId.x + 1, buttonId.y + 1].id.y  = tileNeighbours[buttonId.x + 1, buttonId.y].id.y;
            tileNeighbours[buttonId.x + 1, buttonId.y + 1].angle = (tileNeighbours[buttonId.x + 1, buttonId.y].angle + 1) % 4;

            tileNeighbours[buttonId.x + 1, buttonId.y].id.x  = temp.id.x;
            tileNeighbours[buttonId.x + 1, buttonId.y].id.y  = temp.id.y;
            tileNeighbours[buttonId.x + 1, buttonId.y].angle = (temp.angle + 1) % 4;

            // update buttons
            buttonAngles[buttonId.x, buttonId.y] = (buttonAngles[buttonId.x, buttonId.y] + 1) % 4;
            // check if the puzzle is complete after this rotation
            CheckPuzzleComplete();

            if (saveAfterRotation)
            {
                // some modes do not need immediate saving (e. g. shuffle or autosteps)
                SaveAll();
            }
        }
        // this method parses a status string and sets a TileStatus array according to it
        // it also initializes the TileStatus array if it is not yet initialized
        void ParseStatusString(string statusString, ref TileStatus[,] tiles)
        {
            string[] parts = null;
            if (!string.IsNullOrEmpty(statusString))
            {
                parts = statusString.Split('.');
            }
            int i = 0;

            for (int y = 0; y < descriptor.init.height; y++)
            {
                for (int x = 0; x < descriptor.init.width; x++, i++)
                {
                    TileStatus tileStatus = tiles[x, y];
                    if (tileStatus == null)
                    {
                        tiles[x, y] = tileStatus = new TileStatus
                        {
                            id    = new Vector2Int(x, y),
                            angle = 0
                        };
                    }
                    if (parts != null && i < parts.Length)
                    {
                        string[] statusparts = parts[i].Split(',');
                        if (statusparts.Length == 3)
                        {
                            int tx, ty, angle;
                            if (int.TryParse(statusparts[0], out tx) && int.TryParse(statusparts[1], out ty) && int.TryParse(statusparts[2], out angle))
                            {
                                tileStatus.id.x  = tx;
                                tileStatus.id.y  = ty;
                                tileStatus.angle = angle;
                            }
                        }
                    }
                }
            }
        }
        // restore tiles' positions and angles
        public void RestoreTileStatuses(TileStatus[,] tileNeighbours)
        {
            int     width        = descriptor.init.width;
            int     height       = descriptor.init.height;
            float   tileStartX   = -(width - 1) * TileSize / 2;
            Vector3 tilePosition = new Vector3(tileStartX, (height - 1) * TileSize / 2, neutralTileZ);

            for (int y = 0; y < tileNeighbours.GetUpperBound(1) + 1; y++)
            {
                for (int x = 0; x < tileNeighbours.GetUpperBound(0) + 1; x++)
                {
                    TileStatus tileStatus = tileNeighbours[x, y];
                    GameObject tile       = tiles[tileStatus.id.x, tileStatus.id.y];
                    tile.transform.position      = tilePosition;
                    tile.transform.localRotation = initialTileRotation;
                    tile.transform.Rotate(Vector3.forward, 90f * tileStatus.angle);
                    tilePosition.x += TileSize;
                }
                tilePosition.y -= TileSize;
                tilePosition.x  = tileStartX;
            }
        }