// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:sky' as sky;
import 'dart:math';

import 'package:sky/mojo/activity.dart' as activity;
import 'package:sky/painting/text_style.dart';
import 'package:sky/rendering.dart';
import 'package:sky/theme/colors.dart' as colors;
import 'package:sky/widgets.dart';

// Classic minesweeper-inspired game. The mouse controls are standard
// except for left + right combo which is not implemented. For touch,
// the duration of the pointer determines probing versus flagging.
//
// There are only 3 classes to understand. MineDiggerApp, which is
// contains all the logic and two classes that describe the mines:
// CoveredMineNode and ExposedMineNode, none of them holding state.

// Colors for each mine count (0-8):
const List<TextStyle> textStyles = const <TextStyle>[
  const TextStyle(color: const Color(0xFF555555), fontWeight: bold),
  const TextStyle(color: const Color(0xFF0094FF), fontWeight: bold), // blue
  const TextStyle(color: const Color(0xFF13A023), fontWeight: bold), // green
  const TextStyle(color: const Color(0xFFDA1414), fontWeight: bold), // red
  const TextStyle(color: const Color(0xFF1E2347), fontWeight: bold), // black
  const TextStyle(color: const Color(0xFF7F0037), fontWeight: bold), // dark red
  const TextStyle(color: const Color(0xFF000000), fontWeight: bold),
  const TextStyle(color: const Color(0xFF000000), fontWeight: bold),
  const TextStyle(color: const Color(0xFF000000), fontWeight: bold),
];

enum CellState { covered, exploded, cleared, flagged, shown }

class MineDiggerApp extends App {
  static const int rows = 9;
  static const int cols = 9;
  static const int totalMineCount = 11;

  bool alive;
  bool hasWon;
  int detectedCount;

  // |cells| keeps track of the positions of the mines.
  List<List<bool>> cells;
  // |uiState| keeps track of the visible player progess.
  List<List<CellState>> uiState;

  void initState() {
    resetGame();
  }

  void resetGame() {
    alive = true;
    hasWon = false;
    detectedCount = 0;
    // Build the arrays.
    cells = new List<List<bool>>();
    uiState = new List<List<CellState>>();
    for (int iy = 0; iy != rows; iy++) {
      cells.add(new List<bool>());
      uiState.add(new List<CellState>());
      for (int ix = 0; ix != cols; ix++) {
        cells[iy].add(false);
        uiState[iy].add(CellState.covered);
      }
    }
    // Place the mines.
    Random random = new Random();
    int cellsRemaining = rows * cols;
    int minesRemaining = totalMineCount;
    for (int x = 0; x < cols; x += 1) {
      for (int y = 0; y < rows; y += 1) {
        if (random.nextInt(cellsRemaining) < minesRemaining) {
          cells[y][x] = true;
          minesRemaining -= 1;
          if (minesRemaining <= 0)
            return;
        }
        cellsRemaining -= 1;
      }
    }
    assert(false);
  }

  PointerEventListener _pointerDownHandlerFor(int posX, int posY) {
    return (sky.PointerEvent event) {
      if (event.buttons == 1) {
        probe(posX, posY);
      } else if (event.buttons == 2) {
        flag(posX, posY);
      }
    };
  }

  Widget buildBoard() {
    bool hasCoveredCell = false;
    List<Row> flexRows = <Row>[];
    for (int iy = 0; iy != 9; iy++) {
      List<Widget> row = <Widget>[];
      for (int ix = 0; ix != 9; ix++) {
        CellState state = uiState[iy][ix];
        int count = mineCount(ix, iy);
        if (!alive) {
          if (state != CellState.exploded)
            state = cells[iy][ix] ? CellState.shown : state;
        }
        if (state == CellState.covered) {
          row.add(new GestureDetector(
            onTap: () => probe(ix, iy),
            onLongPress: () {
              activity.userFeedback.performHapticFeedback(activity.HapticFeedbackType_LONG_PRESS);
              flag(ix, iy);
            },
            child: new Listener(
              onPointerDown: _pointerDownHandlerFor(ix, iy),
              child: new CoveredMineNode(
                flagged: false,
                posX: ix,
                posY: iy
              )
            )
          ));
          // Mutating |hasCoveredCell| here is hacky, but convenient, same
          // goes for mutating |hasWon| below.
          hasCoveredCell = true;
        } else if (state == CellState.flagged) {
          row.add(new CoveredMineNode(
            flagged: true,
            posX: ix,
            posY: iy
          ));
        } else {
          row.add(new ExposedMineNode(
            state: state,
            count: count
          ));
        }
      }
      flexRows.add(
        new Row(
          row,
          justifyContent: FlexJustifyContent.center,
          key: new ValueKey<int>(iy)
        )
      );
    }

    if (!hasCoveredCell) {
      // all cells uncovered. Are all mines flagged?
      if ((detectedCount == totalMineCount) && alive) {
        hasWon = true;
      }
    }

    return new Container(
      padding: new EdgeDims.all(10.0),
      margin: new EdgeDims.all(10.0),
      decoration: new BoxDecoration(backgroundColor: const Color(0xFF6B6B6B)),
      child: new Column(flexRows)
    );
  }

  Widget buildToolBar() {
    String toolbarCaption = hasWon ?
      'Awesome!!' : alive ?
        'Mine Digger [$detectedCount-$totalMineCount]': 'Kaboom! [press here]';

    return new ToolBar(
      // FIXME: Strange to have the toolbar be tapable.
      center: new Listener(
        onPointerDown: handleToolbarPointerDown,
        child: new Text(toolbarCaption, style: Theme.of(this).text.title)
      )
    );
  }

  Widget build() {
    // We build the board before we build the toolbar because we compute the win state during build step.
    Widget board = buildBoard();
    return new Title(
      title: 'Mine Digger',
      child: new Scaffold(
        toolbar: buildToolBar(),
        body: new Container(
          child: new Center(child: board),
          decoration: new BoxDecoration(backgroundColor: colors.Grey[50])
        )
      )
    );
  }

  EventDisposition handleToolbarPointerDown(sky.PointerEvent event) {
    setState(() {
      resetGame();
    });
    return EventDisposition.processed;
  }

  // User action. The user uncovers the cell which can cause losing the game.
  void probe(int x, int y) {
    if (!alive)
      return;
    if (uiState[y][x] == CellState.flagged)
      return;
    setState(() {
      // Allowed to probe.
      if (cells[y][x]) {
        // Probed on a mine --> dead!!
        uiState[y][x] = CellState.exploded;
        alive = false;
      } else {
        // No mine, uncover nearby if possible.
        cull(x, y);
      }
    });
  }

  // User action. The user is sure a mine is at this location.
  void flag(int x, int y) {
    setState(() {
      if (uiState[y][x] == CellState.flagged) {
        uiState[y][x] = CellState.covered;
        --detectedCount;
      } else {
        uiState[y][x] = CellState.flagged;
        ++detectedCount;
      }
    });
  }

  // Recursively uncovers cells whose totalMineCount is zero.
  void cull(int x, int y) {
    if ((x < 0) || (x > rows - 1))
      return;
    if ((y < 0) || (y > cols - 1))
      return;

    if (uiState[y][x] == CellState.cleared)
      return;
    uiState[y][x] = CellState.cleared;

    if (mineCount(x, y) > 0)
      return;

    cull(x - 1, y);
    cull(x + 1, y);
    cull(x, y - 1);
    cull(x, y + 1 );
    cull(x - 1, y - 1);
    cull(x + 1, y + 1);
    cull(x + 1, y - 1);
    cull(x - 1, y + 1);
  }

  int mineCount(int x, int y) {
    int count = 0;
    int my = cols - 1;
    int mx = rows - 1;

    count += x > 0 ? bombs(x - 1, y) : 0;
    count += x < mx ? bombs(x + 1, y) : 0;
    count += y > 0 ? bombs(x, y - 1) : 0;
    count += y < my ? bombs(x, y + 1 ) : 0;

    count += (x > 0) && (y > 0) ? bombs(x - 1, y - 1) : 0;
    count += (x < mx) && (y < my) ? bombs(x + 1, y + 1) : 0;
    count += (x < mx) && (y > 0) ? bombs(x + 1, y - 1) : 0;
    count += (x > 0) && (y < my) ? bombs(x - 1, y + 1) : 0;

    return count;
  }

  int bombs(int x, int y) {
    return cells[y][x] ? 1 : 0;
  }
}

Widget buildCell(Widget child) {
  return new Container(
    padding: new EdgeDims.all(1.0),
    height: 27.0, width: 27.0,
    decoration: new BoxDecoration(backgroundColor: const Color(0xFFC0C0C0)),
    margin: new EdgeDims.all(2.0),
    child: child
  );
}

Widget buildInnerCell(Widget child) {
  return new Container(
    padding: new EdgeDims.all(1.0),
    margin: new EdgeDims.all(3.0),
    height: 17.0, width: 17.0,
    child: child
  );
}

class CoveredMineNode extends Component {

  CoveredMineNode({ this.flagged, this.posX, this.posY });

  final bool flagged;
  final int posX;
  final int posY;

  Widget build() {
    Widget text;
    if (flagged)
      text = buildInnerCell(new StyledText(elements : [textStyles[5], '\u2691']));

    Container inner = new Container(
      margin: new EdgeDims.all(2.0),
      height: 17.0, width: 17.0,
      decoration: new BoxDecoration(backgroundColor: const Color(0xFFD9D9D9)),
      child: text
    );

    return buildCell(inner);
  }
}

class ExposedMineNode extends Component {

  ExposedMineNode({ this.state, this.count });

  final CellState state;
  final int count;

  Widget build() {
    StyledText text;
    if (state == CellState.cleared) {
      // Uncovered cell with nearby mine count.
      if (count != 0)
        text = new StyledText(elements : [textStyles[count], '$count']);
    } else {
      // Exploded mine or shown mine for 'game over'.
      int color = state == CellState.exploded ? 3 : 0;
      text = new StyledText(elements : [textStyles[color], '\u2600']);
    }
    return buildCell(buildInnerCell(text));
  }

}

void main() {
  runApp(new MineDiggerApp());
}