using Gameboard.ShogiUI.Sockets.Extensions; using Gameboard.ShogiUI.Sockets.Repositories.CouchModels; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Shogi.Domain; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace Gameboard.ShogiUI.Sockets.Repositories { public interface IGameboardRepository { Task CreateBoardState(Session session); Task CreateSession(SessionMetadata session); Task CreateUser(Models.User user); Task> ReadSessionMetadatas(); Task ReadSession(string name); Task UpdateSession(SessionMetadata session); Task ReadSessionMetaData(string name); Task ReadUser(string userName); } public class GameboardRepository : IGameboardRepository { /// /// Returns session, board state, and user documents, grouped by session. /// private static readonly string View_SessionWithBoardState = "_design/session/_view/session-with-boardstate"; /// /// Returns session and user documents, grouped by session. /// private static readonly string View_SessionMetadata = "_design/session/_view/session-metadata"; private static readonly string View_User = "_design/user/_view/user"; private const string ApplicationJson = "application/json"; private readonly HttpClient client; private readonly ILogger logger; public GameboardRepository(IHttpClientFactory clientFactory, ILogger logger) { client = clientFactory.CreateClient("couchdb"); this.logger = logger; } public async Task> ReadSessionMetadatas() { var queryParams = new QueryBuilder { { "include_docs", "true" } }.ToQueryString(); var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonConvert.DeserializeObject>(responseContent); if (result != null) { var groupedBySession = result.rows.GroupBy(row => row.id); var sessions = new List(result.total_rows / 3); foreach (var group in groupedBySession) { /** * A group contains 3 elements. * 1) The session metadata. * 2) User document of Player1. * 3) User document of Player2. */ var session = group.FirstOrDefault()?.doc.ToObject(); var player1Doc = group.Skip(1).FirstOrDefault()?.doc.ToObject(); var player2Doc = group.Skip(2).FirstOrDefault()?.doc.ToObject(); if (session != null && player1Doc != null) { var player2 = player2Doc == null ? null : new Models.User(player2Doc); sessions.Add(new SessionMetadata(session.Name, session.IsPrivate, player1Doc.Id, player2?.Id)); } } return new Collection(sessions); } return new Collection(Array.Empty()); } public async Task ReadSession(string name) { static Shogi.Domain.Pieces.Piece? MapPiece(Piece? piece) { return piece == null ? null : Shogi.Domain.Pieces.Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); } var queryParams = new QueryBuilder { { "include_docs", "true" }, { "startkey", JsonConvert.SerializeObject(new [] {name}) }, { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } }.ToQueryString(); var query = $"{View_SessionWithBoardState}{queryParams}"; logger.LogInformation("ReadSession() query: {query}", query); var response = await client.GetAsync(query); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonConvert.DeserializeObject>(responseContent); if (result != null && result.rows.Length > 2) { var group = result.rows; /** * A group contains multiple elements. * 0) The session metadata. * 1) User documents of Player1. * 2) User documents of Player1. * 2.a) If the Player2 document doesn't exist, CouchDB will return the SessionDocument instead :( * Everything Else) Snapshots of the boardstate after every player move. */ var session = group[0].doc.ToObject(); var player1Doc = group[1].doc.ToObject(); var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() ? group[2].doc.ToObject() : null; var boardState = group.Last().doc.ToObject(); if (session != null && player1Doc != null && boardState != null) { var player2 = player2Doc == null ? null : new Models.User(player2Doc); var metaData = new SessionMetadata(session.Name, session.IsPrivate, player1Doc.DisplayName, player2Doc?.DisplayName); var shogiBoardState = new BoardState(boardState.Board.ToDictionary(kvp => kvp.Key, kvp => MapPiece(kvp.Value))); return new Session(shogiBoardState, metaData); } } return null; } public async Task ReadSessionMetaData(string name) { var queryParams = new QueryBuilder { { "include_docs", "true" }, { "startkey", JsonConvert.SerializeObject(new [] {name}) }, { "endkey", JsonConvert.SerializeObject(new object [] {name, int.MaxValue}) } }.ToQueryString(); var response = await client.GetAsync($"{View_SessionMetadata}{queryParams}"); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonConvert.DeserializeObject>(responseContent); if (result != null && result.rows.Length > 2) { var group = result.rows; /** * A group contains 3 elements. * 1) The session metadata. * 2) User document of Player1. * 3) User document of Player2. */ var session = group[0].doc.ToObject(); var player1Doc = group[1].doc.ToObject(); var group2DocumentType = group[2].doc.Property(nameof(UserDocument.DocumentType).ToCamelCase())?.Value.Value(); var player2Doc = group2DocumentType == WhichDocumentType.User.ToString() ? group[2].doc.ToObject() : null; if (session != null && player1Doc != null) { var player2 = player2Doc == null ? null : new Models.User(player2Doc); return new SessionMetadata(session.Name, session.IsPrivate, player1Doc.Id, player2?.Id); } } return null; } /// /// Saves a snapshot of board state and the most recent move. /// public async Task CreateBoardState(Session session) { var boardStateDocument = new BoardStateDocument(session.Name, session); var content = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync(string.Empty, content); response.EnsureSuccessStatusCode(); } public async Task CreateSession(SessionMetadata session) { var sessionDocument = new SessionDocument(session); var sessionContent = new StringContent(JsonConvert.SerializeObject(sessionDocument), Encoding.UTF8, ApplicationJson); var postSessionDocumentTask = client.PostAsync(string.Empty, sessionContent); var boardStateDocument = new BoardStateDocument(session.Name, new Session()); var boardStateContent = new StringContent(JsonConvert.SerializeObject(boardStateDocument), Encoding.UTF8, ApplicationJson); if ((await postSessionDocumentTask).IsSuccessStatusCode) { var response = await client.PostAsync(string.Empty, boardStateContent); response.EnsureSuccessStatusCode(); } } public async Task UpdateSession(SessionMetadata session) { // GET existing session to get revisionId. var readResponse = await client.GetAsync(session.Name); readResponse.EnsureSuccessStatusCode(); var sessionDocument = JsonConvert.DeserializeObject(await readResponse.Content.ReadAsStringAsync()); // PUT the document with the revisionId. var couchModel = new SessionDocument(session) { RevisionId = sessionDocument?.RevisionId }; var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var response = await client.PutAsync(couchModel.Id, content); response.EnsureSuccessStatusCode(); } //public async Task PutJoinPublicSession(PutJoinPublicSession request) //{ // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); // var response = await client.PutAsync(JoinSessionRoute, content); // var json = await response.Content.ReadAsStringAsync(); // return JsonConvert.DeserializeObject(json).JoinSucceeded; //} //public async Task PostJoinPrivateSession(PostJoinPrivateSession request) //{ // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); // var response = await client.PostAsync(JoinSessionRoute, content); // var json = await response.Content.ReadAsStringAsync(); // var deserialized = JsonConvert.DeserializeObject(json); // if (deserialized.JoinSucceeded) // { // return deserialized.SessionName; // } // return null; //} //public async Task> GetMoves(string gameName) //{ // var uri = $"Session/{gameName}/Moves"; // var get = await client.GetAsync(Uri.EscapeUriString(uri)); // var json = await get.Content.ReadAsStringAsync(); // if (string.IsNullOrWhiteSpace(json)) // { // return new List(); // } // var response = JsonConvert.DeserializeObject(json); // return response.Moves.Select(m => new Move(m)).ToList(); //} //public async Task PostMove(string gameName, PostMove request) //{ // var uri = $"Session/{gameName}/Move"; // var content = new StringContent(JsonConvert.SerializeObject(request), Encoding.UTF8, MediaType); // await client.PostAsync(Uri.EscapeUriString(uri), content); //} public async Task PostJoinCode(string gameName, string userName) { // var uri = $"JoinCode/{gameName}"; // var serialized = JsonConvert.SerializeObject(new PostJoinCode { PlayerName = userName }); // var content = new StringContent(serialized, Encoding.UTF8, MediaType); // var json = await (await client.PostAsync(Uri.EscapeUriString(uri), content)).Content.ReadAsStringAsync(); // return JsonConvert.DeserializeObject(json).JoinCode; return string.Empty; } public async Task ReadUser(string id) { var queryParams = new QueryBuilder { { "include_docs", "true" }, { "key", JsonConvert.SerializeObject(id) }, }.ToQueryString(); var response = await client.GetAsync($"{View_User}{queryParams}"); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonConvert.DeserializeObject>(responseContent); if (result != null && result.rows.Length > 0) { return new Models.User(result.rows[0].doc); } return null; } public async Task CreateUser(Models.User user) { var couchModel = new UserDocument(user.Id, user.DisplayName, user.LoginPlatform); var content = new StringContent(JsonConvert.SerializeObject(couchModel), Encoding.UTF8, ApplicationJson); var response = await client.PostAsync(string.Empty, content); response.EnsureSuccessStatusCode(); } public void ReadMoveHistory() { //TODO: Separate move history into a separate request. //var moves = group // .Skip(4) // Skip 4 because group[3] will not have a .Move property since it's the first/initial BoardState of the session. // // TODO: Deserialize just the Move property. // .Select(row => row.doc.ToObject()) // .Select(boardState => // { // var move = boardState!.Move!; // return move.PieceFromHand.HasValue // ? new Models.Move(move.PieceFromHand.Value, move.To) // : new Models.Move(move.From!, move.To, move.IsPromotion); // }) // .ToList(); } } }