/*
* Copyright 2015 Daniel Dittmar
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package dan.dit.whatsthat.solution;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import dan.dit.whatsthat.riddle.achievement.AchievementPropertiesMapped;
import dan.dit.whatsthat.riddle.achievement.holders.MiscAchievementHolder;
import dan.dit.whatsthat.testsubject.TestSubject;
import dan.dit.whatsthat.util.compaction.CompactedDataCorruptException;
import dan.dit.whatsthat.util.compaction.Compacter;
/**
* Created by daniel on 12.04.15.
*/
public class SolutionInputLetterClick extends SolutionInput {
private static final char NO_LETTER = '\0'; // this letter is not expected to be in any alphabet
private static final int ALL_LETTERS_AMOUNT_DIVISOR = 6; // must be divisible by this number, related to ALL_LETTERS_MAX_ROWS
private static final int LETTER_POOL_MIN_SIZE = 18; // minimum pool size of all displayed letters
private static final int LETTER_POOL_MIN_WRONG_LETTERS = 2; // minimum amount of wrong letters
public static final String IDENTIFIER = "LETTERCLICK";
private final SolutionInputLetterClickLayout mLayout;
private char[] mAllLetters; // permuted randomly including solution letters
private int[] mAllLettersSelected; // index of letter in user letters if the letter is selected, invisible and one of the user letters
private ArrayList<Character> mUserLetters;
private boolean mStateCompleted;
private boolean mShowCompleted;
private boolean mHintShowMainSolutionWordLength;
public SolutionInputLetterClick(Solution sol) {
super(sol);
mLayout = new SolutionInputLetterClickLayout(this);
}
public SolutionInputLetterClick(Compacter data) throws CompactedDataCorruptException {
super(data);
mLayout = new SolutionInputLetterClickLayout(this);
}
@Override
public void reset() {
clearAllLetters();
}
@Override
public boolean provideHint(int hintLevel) {
switch (hintLevel) {
case HINT_LEVEL_MINIMAL:
return provideSolutionWordLengthHint();
default:
return false;
}
}
@Override
public int getProvidedHintLevel() {
return mHintShowMainSolutionWordLength ? SolutionInput.HINT_LEVEL_MINIMAL :
SolutionInput.HINT_LEVEL_NONE;
}
private boolean provideSolutionWordLengthHint() {
if (!mHintShowMainSolutionWordLength) {
mHintShowMainSolutionWordLength = true;
mLayout.calculateUserLetterLayout();
return true;
}
return false;
}
@Override
public int estimateSolvedValue() {
return mSolution.estimateSolvedValue(userLettersToWord());
}
private int calculateAllLetterAmount(int minLength) {
// ensure a minimum size and a minimum amount of extra letters added to the solution
int amount = Math.max(LETTER_POOL_MIN_SIZE, minLength + LETTER_POOL_MIN_WRONG_LETTERS);
// ensure amount is divisible by 2 and 3
if (amount % ALL_LETTERS_AMOUNT_DIVISOR != 0) {
amount += ALL_LETTERS_AMOUNT_DIVISOR - (amount % ALL_LETTERS_AMOUNT_DIVISOR);
}
return amount;
}
@Override
protected void initSolution(@NonNull Solution solution) {
mSolution = solution;
List<String> solutionWords = mSolution.getWords();
String mainWord = solutionWords.get(0);
String alternativeWord = solutionWords.size() > 1 ? solutionWords.get(1) : null;
int minLetterCount = mainWord.length();
mAllLetters = new char[calculateAllLetterAmount(minLetterCount)];
mAllLettersSelected = new int[mAllLetters.length];
Arrays.fill(mAllLettersSelected, -1);
mUserLetters = new ArrayList<>(mAllLetters.length);
// first init the main word
List<Character> allLetters = new ArrayList<>(mAllLetters.length);
for (int i = 0; i < minLetterCount; i++) {
allLetters.add(mainWord.charAt(i));
}
boolean[] usedMarker = new boolean[minLetterCount + (TextUtils.isEmpty(alternativeWord) ? 0 : alternativeWord.length())];
// then init the alternative word as far as possible, but try to use letters already present to make the alternative word
// only if too little or the wrong letters in main word we add the letters of the alternative word
if (!TextUtils.isEmpty(alternativeWord)) {
for (int i = 0; i < alternativeWord.length() && allLetters.size() < mAllLetters.length; i++) {
char requiredChar = alternativeWord.charAt(i);
boolean foundUnused = false;
for (int j = 0; j < mainWord.length() && !foundUnused; j++) {
if (!usedMarker[j] && allLetters.get(j) == requiredChar) {
foundUnused = true;
usedMarker[j] = true;
}
}
if (!foundUnused) {
minLetterCount++;
allLetters.add(requiredChar);
}
}
}
// fill allLetters with remaining random letters, approximating the
// distribution of letters in the used tongue
Arrays.fill(usedMarker, false);
while (allLetters.size() < mAllLetters.length) {
char nextRandom = mSolution.getTongue().getRandomLetter();
boolean nextRandomMatchedSolutionLetter = false;
for (int j = 0; j < minLetterCount; j++) {
if (allLetters.get(j) == nextRandom && !usedMarker[j]) {
usedMarker[j] = true;
nextRandomMatchedSolutionLetter = true;
break;
}
}
if (!nextRandomMatchedSolutionLetter) {
allLetters.add(nextRandom);
}
}
Collections.shuffle(allLetters);
for (int i = 0; i < allLetters.size(); i++) {
mAllLetters[i] = allLetters.get(i);
}
}
@NonNull
@Override
SolutionInputLayout getLayout() {
return mLayout;
}
private boolean isSolved(String userWord) {
return mSolution.estimateSolvedValue(userWord) == Solution.SOLVED_COMPLETELY;
}
private synchronized void checkCompleted() {
String userWord = userLettersToWord();
if (mStateCompleted) {
if (!isSolved(userWord)) {
mStateCompleted = false;
if (mListener != null) {
mListener.onSolutionIncomplete();
}
}
} else {
// is state incomplete
if (isSolved(userWord)) {
mStateCompleted = true;
if (mListener != null) {
mShowCompleted = mListener.onSolutionComplete(userWord);
}
}
}
}
private int fillLetterInUserLetters(char letter) {
for (int i = 0; i < mUserLetters.size(); i++) {
if (mUserLetters.get(i).equals(NO_LETTER)) {
mUserLetters.set(i, letter);
checkCompleted();
return i;
}
}
mUserLetters.add(letter);
checkCompleted();
return mUserLetters.size() - 1;
}
private int findAllLetterIndex(int userIndex) {
for (int index = 0; index < mAllLetters.length; index++) {
if (mAllLettersSelected[index] == userIndex) {
return index;
}
}
return -1;
}
private void removeAppendedNoLetters() {
int removedCount = 0;
for (int i = mUserLetters.size() - 1; i >= 0; i--) {
if (mUserLetters.get(i).equals(NO_LETTER)) {
removedCount++;
mUserLetters.remove(i);
} else {
break; // stop at the first other letter
}
}
if (removedCount > 0) {
checkCompleted();
}
}
boolean performUserLetterClick(int userLetterIndex) {
if (userLetterIndex < 0 || userLetterIndex >= mUserLetters.size()) {
return false;
}
char clickedChar = mUserLetters.get(userLetterIndex);
if (clickedChar != NO_LETTER) {
int allIndex = findAllLetterIndex(userLetterIndex);
if (allIndex != -1) {
// remove from user selection and make available again
mAllLettersSelected[allIndex] = -1;
mUserLetters.set(userLetterIndex, NO_LETTER);
// remove NO_LETTERs at the end
removeAppendedNoLetters();
mLayout.calculateUserLetterLayout();
return true;
}
} else {
// already not a letter, cut this one out and let cycle following letters left by one
for (int i = userLetterIndex + 1; i < mUserLetters.size(); i++) {
int allIndex = findAllLetterIndex(i);
if (allIndex >= 0) {
mAllLettersSelected[allIndex] = i - 1;
}
}
mUserLetters.remove(userLetterIndex);
removeAppendedNoLetters();
checkCompleted();
mLayout.calculateUserLetterLayout();
return true;
}
return false;
}
boolean performAllLettersClicked(int allLetterIndex) {
if (mAllLettersSelected[allLetterIndex] == -1) {
mAllLettersSelected[allLetterIndex] = fillLetterInUserLetters(mAllLetters[allLetterIndex]);
mLayout.calculateUserLetterLayout();
return true;
}
return false;
}
@Override
public boolean onUserTouchDown(float x, float y) {
return mLayout.executeClick(x, y);
}
private void clearAllLetters() {
List<Integer> all = new ArrayList<>(mUserLetters.size());
for (int i = 0; i < mUserLetters.size(); i++) {
all.add(i);
}
clearLetters(all);
}
private boolean clearLetters(List<Integer> indicesToRemove) {
for (Integer index : indicesToRemove) {
if (index >= mUserLetters.size()) {
continue;
}
char clickedChar = mUserLetters.get(index);
if (clickedChar != NO_LETTER) {
int allIndex = findAllLetterIndex(index);
if (allIndex != -1) {
// remove from user selection and make available again
mAllLettersSelected[allIndex] = -1;
mUserLetters.set(index, NO_LETTER);
}
}
}
if (!indicesToRemove.isEmpty()) {
// remove NO_LETTERs at the end
removeAppendedNoLetters();
mLayout.calculateUserLetterLayout();
return true;
}
return false;
}
boolean showMainSolutionWordLength() {
return mHintShowMainSolutionWordLength;
}
int getMainSolutionWordLength() {
return mSolution.getWords().get(0).length();
}
int getUserLettersCount() {
return mUserLetters.size();
}
int getAllLettersCount() {
return mAllLetters.length;
}
char getUserLetter(int index) {
return mUserLetters.get(index);
}
char getAllLetter(int index) {
return mAllLetters[index];
}
boolean isAllLetterNotSelected(int index) {
return mAllLettersSelected[index] == -1;
}
boolean showCompleted() {
return mShowCompleted;
}
boolean isStateCompleted() {
return mStateCompleted;
}
@Override
public boolean onFling(MotionEvent startEvent, MotionEvent endEvent, float velocityX, float velocityY) {
List<Integer> affectedIndicies = mLayout.getTouchedUserIndicies(startEvent.getX(), startEvent
.getY(), endEvent.getX(), endEvent.getY());
return affectedIndicies != null && clearLetters(affectedIndicies);
}
private String userLettersToWord() {
StringBuilder builder = new StringBuilder(mUserLetters.size());
for (Character c : mUserLetters) {
builder.append(c);
}
String userWord = builder.toString();
if (TestSubject.isInitialized()) {
AchievementPropertiesMapped<String> data = TestSubject.getInstance().getAchievementHolder().getMiscData();
if (data != null) {
data.updateMappedValue(MiscAchievementHolder.KEY_SOLUTION_INPUT_CURRENT_TEXT, userWord);
}
}
return userWord;
}
private void wordToUserLetters(String word) {
mUserLetters = new ArrayList<>(word.length());
for (int i = 0; i < word.length(); i++) {
mUserLetters.add(word.charAt(i));
}
}
@NonNull
@Override
public Solution getCurrentUserSolution() {
String word = userLettersToWord();
if (TextUtils.isEmpty(word)) {
return new Solution(mSolution.getTongue()); // empty solution, should not be the case
}
return new Solution(mSolution.getTongue(), word);
}
@Override
public String compact() {
Compacter cmp = new Compacter();
cmp.appendData(IDENTIFIER);
cmp.appendData(mSolution.compact());
cmp.appendData(userLettersToWord());
cmp.appendData(String.valueOf(mAllLetters));
Compacter cmp2 = new Compacter(mAllLettersSelected.length);
for (int allLetterSelected : mAllLettersSelected) {
cmp2.appendData(allLetterSelected);
}
cmp.appendData(cmp2.compact());
Compacter hintsCompacter = new Compacter(1);
hintsCompacter.appendData(mHintShowMainSolutionWordLength);
cmp.appendData(hintsCompacter.compact());
return cmp.compact();
}
@Override
public void unloadData(Compacter compactedData) throws CompactedDataCorruptException {
if (compactedData == null || compactedData.getSize() < 5) {
throw new CompactedDataCorruptException("Too little data given to build letter click.");
}
mSolution = new Solution(new Compacter(compactedData.getData(1)));
wordToUserLetters(compactedData.getData(2));
String word = compactedData.getData(3);
mAllLetters = word.toCharArray();
mAllLettersSelected = new int[mAllLetters.length];
Compacter inner = new Compacter(compactedData.getData(4));
for (int i = 0; i < inner.getSize(); i++) {
mAllLettersSelected[i] = inner.getInt(i);
}
if (compactedData.getSize() >= 6) {
inner = new Compacter(compactedData.getData(5));
mHintShowMainSolutionWordLength = inner.getBoolean(0);
}
}
}