public class MainActivity extends FragmentActivity {
	private static final String STATE_SHOW_GRAPH = "STATE_SHOW_GRAPH";
	private static final String STATE_SHOW_PLACEMENTS = "STATE_SHOW_PLACEMENTS";
	private static final String STATE_THEFT_ORDER = "STATE_THEFT_ORDER";
	private static final String STATE_EXP_THEFT_ORDER = "STATE_EXP_THEFT_ORDER";
	private static final String STATE_TITLE_ID = "STATE_TITLE_ID";
	
	private static final Bundle GET_PRICES_BUNDLE;
	static {
		ArrayList<String> priceItems = new ArrayList<>();
		priceItems.add(MapContainer.NEW_WORLD.id); // Any normal map should do
		priceItems.add(IabConsts.BUY_ALL);
		
		GET_PRICES_BUNDLE = new Bundle();
		GET_PRICES_BUNDLE.putStringArrayList(IabConsts.GET_SKU_DETAILS_ITEM_LIST, priceItems);
	}

	public static final int BUY_INTENT_REQUEST_CODE = 101;
	
	private MapFragment mMapFragment;
	private GraphFragment mGraphFragment;

	private View mTitle;
	private int mTitleId;
	
	private Set<String> mOwnedMaps = new HashSet<>();
	
	// Stupid onActivityResult is called before onStart()
	// http://stackoverflow.com/q/10114324/452383
	// https://code.google.com/p/android/issues/detail?id=17787
	private boolean mShowFogIsland = false;
	
	private String mSinglePrice = null;
	private String mBuyAllPrice = null;
	
	IInAppBillingService mService;
	private ServiceConnection mServiceConn = new ServiceConnection() {
		@Override
		public void onServiceDisconnected(ComponentName name) {
			mService = null;
		}
		
		@Override
		public void onServiceConnected(ComponentName name, IBinder service) {
			mService = IInAppBillingService.Stub.asInterface(service);
			
            try {
                int response = mService.isBillingSupported(IabConsts.API_VERSION, getPackageName(), IabConsts.ITEM_TYPE_INAPP);
                if (response == IabConsts.BILLING_RESPONSE_RESULT_OK) {
                	BetterLog.w("IAB v" + IabConsts.API_VERSION + " is supported");
    				mMapFragment.setShowSeafarers(true);
                	new InitIabTask().execute();
                } else {
                	BetterLog.w("IAB v" + IabConsts.API_VERSION + " not supported");
    				mMapFragment.setShowSeafarers(true);
                }
            } catch (RemoteException e) {
            	BetterLog.w("Exception while querying for IABv3 support.");
            }
		}
	};
	
	private class InitIabTask extends AsyncTask<Void, Void, Void> {
		@Override
		protected Void doInBackground(Void... params) {
			try {
				restoreTransactions();
				setPrices();
			} catch (RemoteException e) {
				BetterLog.w("RemoteException ", e);
			}
			
			return null;
		}
		
		private void restoreTransactions() throws RemoteException {
			BetterLog.d("RestoreTransactions");
	    	Bundle ownedItems = mService.getPurchases(IabConsts.API_VERSION, getPackageName(),
	    			IabConsts.ITEM_TYPE_INAPP, null);
	        
	        int response = getResponseCodeFromBundle(ownedItems);
	        BetterLog.d("Owned items response: " + String.valueOf(response));
	        if (response != IabConsts.BILLING_RESPONSE_RESULT_OK
	                || !ownedItems.containsKey(IabConsts.RESPONSE_INAPP_ITEM_LIST)
	                || !ownedItems.containsKey(IabConsts.RESPONSE_INAPP_PURCHASE_DATA_LIST)
	                || !ownedItems.containsKey(IabConsts.RESPONSE_INAPP_SIGNATURE_LIST)) {
	        	BetterLog.w("Error querying owned items. Response: " + response);
	        	return;
	        }

	        List<String> purchaseDataList = ownedItems.getStringArrayList(IabConsts.RESPONSE_INAPP_PURCHASE_DATA_LIST);
	        List<String> signatureList = ownedItems.getStringArrayList(IabConsts.RESPONSE_INAPP_SIGNATURE_LIST);
	        
	        for (int i = 0; i < purchaseDataList.size(); i++) {
   	        	verifyAndAddPurchase(purchaseDataList.get(i), signatureList.get(i), true /* restore */);
	        }
		}
	}
	
	private void setPrices() throws RemoteException {
		Bundle details = mService.getSkuDetails(IabConsts.API_VERSION, getPackageName(),
				IabConsts.ITEM_TYPE_INAPP, GET_PRICES_BUNDLE);

        if (!details.containsKey(IabConsts.RESPONSE_GET_SKU_DETAILS_LIST)) {
        	BetterLog.d("Could not fetch prices");
        	return;
        }

        ArrayList<String> responseList = details.getStringArrayList(IabConsts.RESPONSE_GET_SKU_DETAILS_LIST);
        
        for (String response : responseList) {
        	SkuDetails item;
			try {
				item = new SkuDetails(response);
			} catch (JSONException e) {
				BetterLog.w("Could not parse JSON response for finding prices");
				return;
			}
        	
        	if (IabConsts.BUY_ALL.equals(item.sku)) {
        		mBuyAllPrice = item.price;
        	} else {
        		mSinglePrice = item.price;
        	}
        }
	}
	
	public String getSinglePrice() {
		return mSinglePrice;
	}
	
	public String getBuyAllPrice() {
		return mBuyAllPrice;
	}

	private void consumePurchase(Purchase purchase) {
		BetterLog.d("ConsumePurchase");
		try {
			mService.consumePurchase(IabConsts.API_VERSION, getPackageName(), purchase.token);
		} catch (RemoteException e) {
			BetterLog.e("Could not consume purchase");
		}
	}
	
	private void verifyAndAddPurchase(String purchaseData, String signature, boolean restore) {
		BetterLog.d("VerifyAndAddPurchase: " + restore);
		if (Security.verifyPurchase(purchaseData, signature)) {
			BetterLog.d("Passed security");
			Purchase purchase;
			try {
				purchase = new Purchase(purchaseData, signature);
			} catch (JSONException e) {
				BetterLog.e("Unable to parse returned JSON. Not adding item");
				return;
			}
			
			// If it's prod, ID=SKU
			// If we're testing, ID is stored in devPayload
    		String itemId = Consts.TEST_STATIC_IAB ? purchase.developerPayload : purchase.sku;
			BetterLog.d("Purchasing " + itemId);
    		
			mOwnedMaps.add(itemId);

    		// If we're testing IAB, consume all purchases immediately so we can test infinitely
        	if (Consts.TEST_CONSUME_ALL_PURCHASES) {
        		consumePurchase(purchase);
        	}
			
			// If we're restoring all purchases, don't do anything special
			if (restore) {
				return;
			}
			
			// Else show the map they chose, and maybe Fog Island help
			if (getMapFragment() != null && !itemId.equals(IabConsts.BUY_ALL)) {
				getMapFragment().sizeChoice(MapSize.getMapSizeByProductId(itemId));
			}
			
			// Show Fog Island if we need to (and we're not restoring all maps)
			// Note the comment on mShowFogIsland (we need to wait until after onStart() before
			// showing the fragment
			if (MapContainer.THE_FOG_ISLAND.id.equals(itemId)) {
				mShowFogIsland = true;
			}
    	}
	}
	
	public void purchaseItem(MapContainer map) {
		purchaseItem(map.id);
	}
	
	public void purchaseItem(String id) {
		BetterLog.i("Buying " + id);
		
		// If it's prod, use real ID and no devPayload
		// If we're testing, use fake product id and store ID in devPayload
		String sku = Consts.TEST_STATIC_IAB ? IabConsts.FAKE_PRODUCT_ID : id;
		String devPayload = Consts.TEST_STATIC_IAB ? id : null;
		
    	Bundle buyIntentBundle;
    	try {
			buyIntentBundle = mService.getBuyIntent(IabConsts.API_VERSION, getPackageName(), sku,
					IabConsts.ITEM_TYPE_INAPP, devPayload);
		} catch (RemoteException e) {
			BetterLog.w("RemoteException", e);
			return;
		}

		int response = getResponseCodeFromBundle(buyIntentBundle);
		if (response != IabConsts.BILLING_RESPONSE_RESULT_OK) {
			BetterLog.e("Bad response: " + response);
			return;
		}
		
		PendingIntent buyIntent = buyIntentBundle.getParcelable(IabConsts.RESPONSE_BUY_INTENT);
		if (buyIntent == null) {
			BetterLog.w("Item has already been purchased");
			return;
		}
		
		try {
			startIntentSenderForResult(buyIntent.getIntentSender(), BUY_INTENT_REQUEST_CODE,
					new Intent(), 0, 0, 0);
		} catch (SendIntentException e) {
			BetterLog.e("Error sending intent", e);
		}
	}

	
	// Workaround to bug where sometimes response codes come as Long instead of Integer
	private int getResponseCodeFromBundle(Bundle b) {
        Object o = b.get(IabConsts.RESPONSE_CODE);
        if (o == null) {
            BetterLog.i("Bundle with null response code, assuming OK (known issue)");
            return IabConsts.BILLING_RESPONSE_RESULT_OK;
        } else if (o instanceof Integer) {
        	return ((Integer)o).intValue();
        } else if (o instanceof Long) {
        	return (int)((Long)o).longValue();
        } else {
        	BetterLog.e("Unexpected type for bundle response code.");
        	BetterLog.e(o.getClass().getName());
            throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    	switch (requestCode) {
    	case BUY_INTENT_REQUEST_CODE:
    		// Verify/consume the purchase
    		if (data == null) {
    			BetterLog.w("Null data. Returning");
    			return;
    		}
        	
            String purchaseData = data.getStringExtra(IabConsts.RESPONSE_INAPP_PURCHASE_DATA);
            String dataSignature = data.getStringExtra(IabConsts.RESPONSE_INAPP_SIGNATURE);
            
            if (resultCode == RESULT_OK) {
            	BetterLog.d("Successful return code from purchase activity.");
            	verifyAndAddPurchase(purchaseData, dataSignature, false /* restore */);
            } else if (resultCode == RESULT_CANCELED) {
            	BetterLog.w("Purchase canceled");
            } else {
            	BetterLog.w("Purchase failed");
            }
    		break;
    	}
    }

	/** Called when the activity is going to disappear. */
	@Override
	public void onSaveInstanceState(Bundle outState) {
		BetterLog.d("MainActivity.onSaveInstanceState");
		
		if (mGraphFragment.isVisible()) {
			outState.putBoolean(STATE_SHOW_GRAPH, true);
		} else if (mMapFragment.isVisible()) {
			if (mMapFragment.showingPlacements()) {
				outState.putBoolean(STATE_SHOW_PLACEMENTS, true);
			}
		}
		
		if (mMapFragment != null && mMapFragment.getCatanMap().theftOrder != null
				&& !mMapFragment.getCatanMap().theftOrder.isEmpty()) {
			if (mMapFragment.getCatanMap().name.equals("new_world")) {
				outState.putIntegerArrayList(STATE_THEFT_ORDER, mMapFragment.getCatanMap().theftOrder);
			}
			if (mMapFragment.getCatanMap().name.equals("new_world_exp")) {
				outState.putIntegerArrayList(STATE_EXP_THEFT_ORDER, mMapFragment.getCatanMap().theftOrder);
			}
		}
		
		outState.putInt(STATE_TITLE_ID, mTitleId);
		super.onSaveInstanceState(outState);
	}
	
	/** Called when the activity is first created. */
	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		setContentView(R.layout.main);
		
		mMapFragment = (MapFragment) getSupportFragmentManager().findFragmentById(R.id.map_fragment);
		mGraphFragment = (GraphFragment) getSupportFragmentManager().findFragmentById(R.id.graph_fragment);

		mTitle = findViewById(R.id.title_text);
		if (savedInstanceState != null && savedInstanceState.getInt(STATE_TITLE_ID) != 0) {
			setTitleButtonText(savedInstanceState.getInt(STATE_TITLE_ID));
		} else {
			setTitleButtonText(MapSize.STANDARD.titleDrawableId);
		}
		
		ImageView infoButton = (ImageView) findViewById(R.id.info_button);
		infoButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				AboutDialogFragment.newInstance().show(getSupportFragmentManager(), "AboutDialog");
				Analytics.track(MainActivity.this, Analytics.CATEGORY_MAIN, Analytics.ACTION_BUTTON,
                        Analytics.INFO);
			}
		});

		boolean showGraph = false;
		boolean showPlacements = false;
		if (savedInstanceState != null && savedInstanceState.getBoolean(STATE_SHOW_GRAPH)) {
			showGraph = true;
		} else if (savedInstanceState != null && savedInstanceState.getBoolean(STATE_SHOW_PLACEMENTS)) {
			showPlacements = true;
		}

		if (showGraph) {
			showGraphFragment();
		} else {
			showMapFragment();
			if (showPlacements) {
				getMapFragment().showPlacements(false);
			} else {
				getMapFragment().hidePlacements(false);
			}
		}
		
		mOwnedMaps.add(MapContainer.HEADING_FOR_NEW_SHORES.id);

		// IAB
        Intent serviceIntent = new Intent(IabConsts.BIND_ACTION);
        serviceIntent.setPackage(IabConsts.VENDING_PACKAGE);
		bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
	}
	
	@Override
	public void onResumeFragments() {
		super.onResumeFragments();
		
		if (mShowFogIsland) {
			mShowFogIsland = false;

			SharedPreferences prefs = getSharedPreferences(Consts.SHARED_PREFS_NAME, Context.MODE_PRIVATE);
			boolean shownWhatsNew = prefs.getBoolean(Consts.SHARED_PREFS_KEY_FOG_ISLAND_HELP, false);
			if (!shownWhatsNew) {
				FogIslandHelpDialogFragment.newInstance().show(getSupportFragmentManager(), "TheFogIslandHelpDialog");
				SharedPreferences.Editor prefsEditor = prefs.edit();
				prefsEditor.putBoolean(Consts.SHARED_PREFS_KEY_FOG_ISLAND_HELP, true);
				prefsEditor.apply();
			}
		}
	}
	
	@Override
	public void onDestroy() {
		super.onDestroy();
		
		// IAB
		if (mService != null) {
			unbindService(mServiceConn);
			mService = null;
		}
	}
    
    public void setTitleButtonText(final int resId) {
    	runOnUiThread(new Runnable() {
			@Override
			public void run() {
		    	mTitle.setBackgroundResource(resId);
			}
		});
    	mTitleId = resId;
    }
    
    public Set<String> getOwnedMaps() {
    	return mOwnedMaps;
    }
    
    public MapFragment getMapFragment() {
    	return (MapFragment) getSupportFragmentManager().findFragmentById(R.id.map_fragment);
    }
    
    public GraphFragment getGraphFragment() {
    	return (GraphFragment) getSupportFragmentManager().findFragmentById(R.id.graph_fragment);
    }
    
    public void showMapFragment() {
		FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
		//ft.setCustomAnimations(R.anim.long_slide_in_left, R.anim.long_slide_out_right);
		ft.hide(mGraphFragment);
		ft.show(mMapFragment);
		ft.commit();
    }
    
    public void showGraphFragment() {
		FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
		//ft.setCustomAnimations(R.anim.long_slide_in_right, R.anim.long_slide_out_left);
		ft.hide(mMapFragment);
		ft.show(mGraphFragment);
		ft.addToBackStack("GraphFragment");
		ft.commit();

		
		Analytics.trackView(this, Analytics.VIEW_ROLL_TRACKER);
    }
} public final class CatanMap {
	/** The name of this map. */
	public final String name;
	
	/** The display title of this map. */
	public final String title;

	/** How many rocks/clays there are available on to distribute. */
	public final int lowResourceNumber;

	/** How many wheats/woods/sheep there are available on to distribute. */
	public final int highResourceNumber;

	/** The assigned probability for each point (if it exists). */
	public final int[] landGridProbabilities;

	/** The assigned resource for each point (if it exists). */
	public final Resource[] landGridResources;

	/** The (x,y) coordinates of the each land hexagon. */
	public final Point[] landGrid;

	/** The list of whitelists for each point (if it exists). */
	public final String[] landGridWhitelists;

	/** The whitelist of resources that can be on a certain land grid piece */
	public final Map<String, List<Resource>> landResourceWhitelists;

	/** The whitelist of probabilities that can be on a certain land grid piece */
	public final Map<String, List<Integer>> landProbabilityWhitelists;

	/** The order in which the land grid is laid out. */
	public final int[] landGridOrder;

	/** The (x,y) coordinates of each water hexagon. */
	public final Point[] waterGrid;

	/**
	 * This list contains 2 or 3 numbers that are the possible corners that the lines can go to
	 * (half only touch two at 2 corners and half touch at 3 corners).  0 is the TL corner, 1
	 * is the top corner, 2 is the TR corner, and so forth clockwise around the hexagon to 5 (BL).
	 */
	public final int[][] harborLines;

	/**
	 * This list of lists contains the land tiles that each land tile is neighbors with.
	 * The land tiles are numbered 0-18 starting at the TL corner and going L -> R, T -> B.
	 */
	public final int[][] landNeighbors;

	/**
	 * This list of lists contains the land tiles that each ocean tile is neighbors with.
	 * The ocean tiles are (similar to everywhere else) numbered starting at the TL ocean
	 * tile and going around clockwise 0-17.  The land tiles are (similar to everywhere else)
	 * numbered 0-18 starting at the TL land tile and going L -> R, T -> B.
	 * Note that the numbers are not small to large but are numbered clockwise by whichever land
	 * tile is earliest on in the clockwise rotation (starting in the TL corner).
	 */
	public final int[][] waterNeighbors;

	/**
	 * This list of list is like waterNeighbors except it contains the ocean tiles each ocean tile
	 * is neighbors with (not including itself)
	 */
	public final int[][] waterWaterNeighbors;

	/**
	 * "Triplets" are defined as three terrain tiles that come together at an intersection
	 * (ports do not count). These are ordered starting in the TL corner going L -> R, T -> B
	 * (going straight across such that the top three terrain tiles are the "top two" for the
	 * first triplets. These are defined so that we can make sure no single settlement placement
	 * is too amazing.
	 */
	public final int[][] landIntersections;

	/** This is a mapping between STANDARD_LAND_GRID and STANDARD_LAND_INTERSECTIONS. */
	public final int[][] landIntersectionIndexes;

	/** 
	 * This is a way to identify places in between the hexes. This is not a unique mapping, but
	 * the X represents which hex its referring to and the Y represents which direction off of
	 * the hex its pointing.
	 */
	public final int[][] placementIndexes;

	/** List of how many of each resource this type of board contains. */
	public final Resource[] availableResources;

	/** List of how many of each probability this type of board contains. */
	public final int[] availableProbabilities;

	/** List of how many of each probability this type of board contains. */
	public final int[] availableOrderedProbabilities;

	/** List of how many of harbors there are (desert is 3:1). */
	public final Resource[] availableHarbors;

	/** Hardcoded directions of harbors on "traditional" maps.  Hardcoded since they're weird. */
	public final int[] orderedHarbors;

	/** The (x,y) coordinates of the each unknown hexagon. */
	public final Point[] unknownGrid;

	/** List of how many of each resource this type of board contains for unknown only. */
	public final Resource[] availableUnknownResources;

	/** List of how many of each probability this type of board contains for unknown only. */
	public final int[] availableUnknownProbabilities;

	/** List of which placement degrees around each piece are disallowed. Null if no blacklist. */
	public final List<int[]> placementBlacklists;

	/** Order of any land converted to water so we can keep the same map for New World on a rotation. */
	public final ArrayList<Integer> theftOrder;
	
	private CatanMap(Builder builder) {
		this.name = builder.name;
		this.title = builder.title;
		this.lowResourceNumber = builder.lowResourceNumber;
		this.highResourceNumber = builder.highResourceNumber;
		this.landGridProbabilities = builder.landGridProbabilities;
		this.landGridResources = builder.landGridResources;
		this.landGrid = builder.landGrid;
		this.landGridWhitelists = builder.landGridWhitelists;
		this.landResourceWhitelists = builder.landResourceWhitelists;
		this.landProbabilityWhitelists = builder.landProbabilityWhitelists;
		this.landGridOrder = builder.landGridOrder;
		this.waterGrid = builder.waterGrid;
		this.harborLines = builder.harborLines;
		this.landNeighbors = builder.landNeighbors;
		this.waterNeighbors = builder.waterNeighbors;
		this.waterWaterNeighbors = builder.waterWaterNeighbors;
		this.landIntersections = builder.landIntersections;
		this.landIntersectionIndexes = builder.landIntersectionIndexes;
		this.placementIndexes = builder.placementIndexes;
		this.availableResources = builder.availableResources;
		this.availableProbabilities = builder.availableProbabilities;
		this.availableOrderedProbabilities = builder.availableOrderedProbabilities;
		this.availableHarbors = builder.availableHarbors;
		this.orderedHarbors = builder.orderedHarbors;
		this.unknownGrid = builder.unknownGrid;
		this.availableUnknownResources = builder.availableUnknownResources;
		this.availableUnknownProbabilities = builder.availableUnknownProbabilities;
		this.placementBlacklists = builder.placementBlacklists;
		this.theftOrder = builder.theftOrder;
	}
	
	@Override
	public boolean equals(Object other) {
		if (this == other) {
			return true;
		}
		if (!(other instanceof CatanMap)) {
			return false;
		}
		CatanMap that = (CatanMap) other;
		
		return Util.equal(this.name, that.name)
		    && Util.equal(this.title, that.title)
		    && Util.equal(this.lowResourceNumber, that.lowResourceNumber)
		    && Util.equal(this.highResourceNumber, that.highResourceNumber)
		    && Util.equal(this.landGridProbabilities, that.landGridProbabilities)
		    && Util.equal(this.landGridResources, that.landGridResources)
		    && Util.equal(this.landGrid, that.landGrid)
		    && Util.equal(this.landGridWhitelists, that.landGridWhitelists)
		    && Util.equal(this.landResourceWhitelists, that.landResourceWhitelists)
		    && Util.equal(this.landProbabilityWhitelists, that.landProbabilityWhitelists)
		    && Util.equal(this.landGridOrder, that.landGridOrder)
		    && Util.equal(this.waterGrid, that.waterGrid)
		    && Util.equal(this.harborLines, that.harborLines)
		    && Util.equal(this.landNeighbors, that.landNeighbors)
		    && Util.equal(this.waterNeighbors, that.waterNeighbors)
		    && Util.equal(this.waterWaterNeighbors, that.waterWaterNeighbors)
		    && Util.equal(this.landIntersections, that.landIntersections)
		    && Util.equal(this.landIntersectionIndexes, that.landIntersectionIndexes)
		    && Util.equal(this.placementIndexes, that.placementIndexes)
		    && Util.equal(this.availableResources, that.availableResources)
		    && Util.equal(this.availableProbabilities, that.availableProbabilities)
		    && Util.equal(this.availableOrderedProbabilities, that.availableOrderedProbabilities)
		    && Util.equal(this.availableHarbors, that.availableHarbors)
		    && Util.equal(this.orderedHarbors, that.orderedHarbors)
		    && Util.equal(this.unknownGrid, that.unknownGrid)
		    && Util.equal(this.availableUnknownResources, that.availableUnknownResources)
		    && Util.equal(this.availableUnknownProbabilities, that.availableUnknownProbabilities)
		    && Util.equal(this.placementBlacklists, that.placementBlacklists)
		    && Util.equal(this.theftOrder, that.theftOrder);
	}
	
	@Override
	public int hashCode() {
		return Util.hashCode(name, title, lowResourceNumber, highResourceNumber,
				landGridProbabilities, landGridResources, landGrid, landGridWhitelists,
				landResourceWhitelists, landGridOrder, waterGrid, harborLines, landNeighbors,
				waterNeighbors, waterWaterNeighbors, landIntersections, landIntersectionIndexes,
				placementIndexes, availableResources, availableProbabilities,
				availableOrderedProbabilities, availableHarbors, orderedHarbors,
				unknownGrid, availableUnknownResources, availableUnknownProbabilities,
				placementBlacklists, theftOrder);
	}
	
	private String deepToString(List<int[]> array) {
		StringBuilder sb = new StringBuilder();
		sb.append("[ ");
		for (int[] arr : array) {
			sb.append(Arrays.toString(arr)).append(", ");
		}
		sb.append(" ]");
		return sb.toString();
	}
	
	@Override
	public String toString() {
		return "[Settlers Map: (" + name + ")" + "\n" +
				"  Low Resource Number: " + lowResourceNumber + "\n" +
				"  High Resource Number: " + highResourceNumber + "\n" +
				"  Land Grid: " + Arrays.toString(landGrid) + "\n" +
				"  Land Grid Whitelist: " + Arrays.toString(landGridWhitelists) + "\n" +
				"  Land Grid Probabilities: " + Arrays.toString(landGridProbabilities) + "\n" +
				"  Land Grid Resources: " + Arrays.toString(landGridResources) + "\n" +
				"  Land Resource Whitelist: " + landResourceWhitelists + "\n" +
				"  Land Probability Whitelist: " + landProbabilityWhitelists + "\n" +
				"  Land Grid Order: " + Arrays.toString(landGridOrder) + "\n" +
				"  Water Grid: " + Arrays.toString(waterGrid) + "\n" +
				"  Harbor Lines: " + Arrays.deepToString(harborLines) + "\n" +
				"  Land Neighbors: " + Arrays.deepToString(landNeighbors) + "\n" +
				"  Water Neighbors: " + Arrays.deepToString(waterNeighbors) + "\n" +
				"  Water Water Neighbors: " + Arrays.deepToString(waterWaterNeighbors) + "\n" +
				"  Land Intersections: " + Arrays.deepToString(landIntersections) + "\n" +
				"  Land Intersections Size: " + landIntersections.length + "\n" +
				"  Land Intersection Indexes: " + Arrays.deepToString(landIntersectionIndexes) + "\n" +
				"  Placement Indexes: " + Arrays.deepToString(placementIndexes) + "\n" +
				"  Placement Indexes Size: " + placementIndexes.length + "\n" +
				"  Available Resources: " + Arrays.toString(availableResources) + "\n" +
				"  Available Probabilities: " + Arrays.toString(availableProbabilities) + "\n" +
				"  Available Ordered Probabilities: " + Arrays.toString(availableOrderedProbabilities) + "\n" +
				"  Available Harbors: " + Arrays.toString(availableHarbors) + "\n" +
				"  Ordered Harbors: " + Arrays.toString(orderedHarbors) + "\n" +
				"  Placement Blacklists: " + deepToString(placementBlacklists) + "\n" +
				"]";
	}
	
	public static Builder newBuilder() {
		return new Builder();
	}
	
	public static class Builder {
		private String name;
		private String title;
		private int lowResourceNumber;
		private int highResourceNumber;
		private int[] landGridProbabilities;
		private Resource[] landGridResources;
		private Point[] landGrid;
		private String[] landGridWhitelists;
		private Map<String, List<Resource>> landResourceWhitelists;
		private Map<String, List<Integer>> landProbabilityWhitelists;
		private int[] landGridOrder;
		private Point[] waterGrid;
		private int[][] harborLines;
		private int[][] landNeighbors;
		private int[][] waterNeighbors;
		private int[][] waterWaterNeighbors;
		private int[][] landIntersections;
		private int[][] landIntersectionIndexes;
		private int[][] placementIndexes;
		private Resource[] availableResources;
		private int[] availableProbabilities;
		private int[] availableOrderedProbabilities;
		private Resource[] availableHarbors;
		private int[] orderedHarbors;
		private Point[] unknownGrid;
		private Resource[] availableUnknownResources;
		private int[] availableUnknownProbabilities;
		private List<int[]> placementBlacklists;
		private ArrayList<Integer> theftOrder;
		
		private Builder() {}
		
		public CatanMap build() {
			return new CatanMap(this);
		}
		
		public String getName() {
			return name;
		}

		public String getTitle() {
			return title;
		}

		public int getLowResourceNumber() {
			return lowResourceNumber;
		}

		public int getHighResourceNumber() {
			return highResourceNumber;
		}

		public int[] getLandGridProbabilities() {
			return landGridProbabilities;
		}

		public Resource[] getLandGridResources() {
			return landGridResources;
		}

		public Point[] getLandGrid() {
			return landGrid;
		}

		public String[] getLandGridWhitelists() {
			return landGridWhitelists;
		}

		public Map<String, List<Resource>> getLandResourceWhitelists() {
			return landResourceWhitelists;
		}

		public Map<String, List<Integer>> getLandProbabilityWhitelists() {
			return landProbabilityWhitelists;
		}

		public int[] getLandGridOrder() {
			return landGridOrder;
		}

		public Point[] getWaterGrid() {
			return waterGrid;
		}

		public int[][] getHarborLines() {
			return harborLines;
		}

		public int[][] getLandNeighbors() {
			return landNeighbors;
		}

		public int[][] getWaterNeighbors() {
			return waterNeighbors;
		}

		public int[][] getWaterWaterNeighbors() {
			return waterWaterNeighbors;
		}

		public int[][] getLandIntersections() {
			return landIntersections;
		}

		public int[][] getLandIntersectionIndexes() {
			return landIntersectionIndexes;
		}

		public int[][] getPlacementIndexes() {
			return placementIndexes;
		}

		public Resource[] getAvailableResources() {
			return availableResources;
		}

		public int[] getAvailableProbabilities() {
			return availableProbabilities;
		}

		public int[] getAvailableOrderedProbabilities() {
			return availableOrderedProbabilities;
		}

		public Resource[] getAvailableHarbors() {
			return availableHarbors;
		}

		public int[] getOrderedHarbors() {
			return orderedHarbors;
		}

		public Point[] getUnknownGrid() {
			return unknownGrid;
		}

		public Resource[] getAvailableUnknownResources() {
			return availableUnknownResources;
		}

		public int[] getAvailableUnknownProbabilities() {
			return availableUnknownProbabilities;
		}

		public List<int[]> getPlacementBlacklists() {
			return placementBlacklists;
		}

		public ArrayList<Integer> getTheftOrder() {
			return theftOrder;
		}

		public Builder setName(String name) {
			this.name = name;
			return this;
		}

		public Builder setTitle(String title) {
			this.title = title;
			return this;
		}

		public Builder setLowResourceNumber(int lowResourceNumber) {
			this.lowResourceNumber = lowResourceNumber;
			return this;
		}

		public Builder setHighResourceNumber(int highResourceNumber) {
			this.highResourceNumber = highResourceNumber;
			return this;
		}

		public Builder setLandGridProbabilities(int[] landGridProbabilities) {
			this.landGridProbabilities = landGridProbabilities;
			return this;
		}

		public Builder setLandGridResources(Resource[] landGridResources) {
			this.landGridResources = landGridResources;
			return this;
		}

		public Builder setLandGrid(Point[] landGrid) {
			this.landGrid = landGrid;
			return this;
		}

		public Builder setLandGridWhitelists(String[] landGridWhitelists) {
			this.landGridWhitelists = landGridWhitelists;
			return this;
		}

		public Builder setLandResourceWhitelists(
				Map<String, List<Resource>> landResourceWhitelists) {
			this.landResourceWhitelists = landResourceWhitelists;
			return this;
		}

		public Builder setLandProbabilityWhitelists(
				Map<String, List<Integer>> landProbabilityWhitelists) {
			this.landProbabilityWhitelists = landProbabilityWhitelists;
			return this;
		}

		public Builder setLandGridOrder(int[] landGridOrder) {
			this.landGridOrder = landGridOrder;
			return this;
		}

		public Builder setWaterGrid(Point[] waterGrid) {
			this.waterGrid = waterGrid;
			return this;
		}

		public Builder setHarborLines(int[][] harborLines) {
			this.harborLines = harborLines;
			return this;
		}

		public Builder setLandNeighbors(int[][] landNeighbors) {
			this.landNeighbors = landNeighbors;
			return this;
		}

		public Builder setWaterNeighbors(int[][] waterNeighbors) {
			this.waterNeighbors = waterNeighbors;
			return this;
		}

		public Builder setWaterWaterNeighbors(int[][] waterWaterNeighbors) {
			this.waterWaterNeighbors = waterWaterNeighbors;
			return this;
		}

		public Builder setLandIntersections(int[][] landIntersections) {
			this.landIntersections = landIntersections;
			return this;
		}

		public Builder setLandIntersectionIndexes(int[][] landIntersectionIndexes) {
			this.landIntersectionIndexes = landIntersectionIndexes;
			return this;
		}

		public Builder setPlacementIndexes(int[][] placementIndexes) {
			this.placementIndexes = placementIndexes;
			return this;
		}

		public Builder setAvailableResources(Resource[] availableResources) {
			this.availableResources = availableResources;
			return this;
		}

		public Builder setAvailableProbabilities(int[] availableProbabilities) {
			this.availableProbabilities = availableProbabilities;
			return this;
		}

		public Builder setAvailableOrderedProbabilities(int[] availableOrderedProbabilities) {
			this.availableOrderedProbabilities = availableOrderedProbabilities;
			return this;
		}

		public Builder setAvailableHarbors(Resource[] availableHarbors) {
			this.availableHarbors = availableHarbors;
			return this;
		}

		public Builder setOrderedHarbors(int[] orderedHarbors) {
			this.orderedHarbors = orderedHarbors;
			return this;
		}

		public Builder setUnknownGrid(Point[] unknownGrid) {
			this.unknownGrid = unknownGrid;
			return this;
		}

		public Builder setAvailableUnknownResources(Resource[] availableUnknownResources) {
			this.availableUnknownResources = availableUnknownResources;
			return this;
		}

		public Builder setAvailableUnknownProbabilities(int[] availableUnknownProbabilities) {
			this.availableUnknownProbabilities = availableUnknownProbabilities;
			return this;
		}

		public Builder setPlacementBlacklists(List<int[]> placementBlacklists) {
			this.placementBlacklists = placementBlacklists;
			return this;
		}

		public Builder setTheftOrder(ArrayList<Integer> theftOrder) {
			this.theftOrder = theftOrder;
			return this;
		}
	}
} public final class Harbor implements Parcelable {
	public final int position;
	public final Resource resource;
	public final int facing;
	
	public Harbor(int position, Resource resource, int facing) {
		this.position = position;
		this.resource = resource;
		this.facing = facing;
	}
	
	private Harbor(Parcel in) {
		position = in.readInt();
		resource = in.readParcelable(getClass().getClassLoader());
		facing = in.readInt();
	}
	
	@Override
	public boolean equals(Object other) {
		if (this == other) {
			return true;
		}
		if (!(other instanceof Harbor)) {
			return false;
		}
		Harbor that = (Harbor) other;
		
		return Util.equal(this.position, that.position)
		    && Util.equal(this.resource, that.resource)
		    && Util.equal(this.facing, that.facing);
	}
	
	@Override
	public int hashCode() {
		return Util.hashCode(position, resource, facing);
	}

    public static final Parcelable.Creator<Harbor> CREATOR = new Parcelable.Creator<Harbor>() {
        public Harbor createFromParcel(Parcel in) {
            return new Harbor(in);
        }

        public Harbor[] newArray(int size) {
            return new Harbor[size];
        }
    };
	
	@Override
	public int describeContents() {
        return 0;
    }

	@Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(position);
        out.writeParcelable(resource, flags);
        out.writeInt(facing);
    }
	
	@Override
	public String toString() {
		return "[Harbor:" +
				" position: " + position +
				" resource: " + resource +
				" facing:   " + facing +
				" ]"+ "\n";
	}
} public enum MapSize implements Parcelable {
	STANDARD(new Standard(), "standard", R.drawable.title_settlers, "standard"),
	LARGE(new Large(), "large", R.drawable.title_settlers, "large"),
	XLARGE(new XLarge(), "xlarge", R.drawable.title_settlers, "xlarge"),
	
	HEADING_FOR_NEW_SHORES(new HeadingForNewShores(), "heading_for_new_shores",
			R.drawable.title_heading_for_new_shores, "seafarers.heading_for_new_shores"),
	THE_FOUR_ISLANDS(new TheFourIslands(), "the_four_islands",
			R.drawable.title_the_four_islands, "seafarers.the_four_islands"),
	THE_FOG_ISLAND(new TheFogIsland(), "the_fog_island",
			R.drawable.title_the_fog_island, "seafarers.the_fog_island"),
	THROUGH_THE_DESERT(new ThroughTheDesert(), "through_the_desert",
			R.drawable.title_through_the_desert, "seafarers.through_the_desert"),
	THE_FORGOTTEN_TRIBE(new TheForgottenTribe(), "the_forgotten_tribe",
			R.drawable.title_the_forgotten_tribe, "seafarers.the_forgotten_tribe"),
	CLOTH_FOR_CATAN(new ClothForCatan(), "cloth_for_catan",
			R.drawable.title_cloth_for_catan, "seafarers.cloth_for_catan"),
	THE_PIRATE_ISLANDS(new ThePirateIslands(), "the_pirate_islands",
			R.drawable.title_the_pirate_islands, "seafarers.the_pirate_islands"),
	THE_WONDERS_OF_CATAN(new TheWondersOfCatan(), "the_wonders_of_catan",
			R.drawable.title_the_wonders_of_catan, "seafarers.the_wonders_of_catan"),
	NEW_WORLD(new NewWorld(), "new_world", R.drawable.title_new_world, "seafarers.new_world"),

	HEADING_FOR_NEW_SHORES_EXP(new HeadingForNewShoresExp(), "heading_for_new_shores_exp",
			R.drawable.title_heading_for_new_shores, null),
	THE_FOUR_ISLANDS_EXP(new TheFourIslandsExp(), "the_four_islands_exp",
			R.drawable.title_the_four_islands, null),
	THE_FOG_ISLAND_EXP(new TheFogIslandExp(), "the_fog_island_exp",
			R.drawable.title_the_fog_island, null),
	THROUGH_THE_DESERT_EXP(new ThroughTheDesertExp(), "through_the_desert_exp",
			R.drawable.title_through_the_desert, null),
	THE_FORGOTTEN_TRIBE_EXP(new TheForgottenTribeExp(), "the_forgotten_tribe_exp",
			R.drawable.title_the_forgotten_tribe, null),
	CLOTH_FOR_CATAN_EXP(new ClothForCatanExp(), "cloth_for_catan_exp",
			R.drawable.title_cloth_for_catan, null),
	THE_PIRATE_ISLANDS_EXP(new ThePirateIslandsExp(), "the_pirate_islands_exp",
			R.drawable.title_the_pirate_islands, null),
	THE_WONDERS_OF_CATAN_EXP(new TheWondersOfCatanExp(), "the_wonders_of_catan_exp",
			R.drawable.title_the_wonders_of_catan, null),
	NEW_WORLD_EXP(new NewWorldExp(), "new_world_exp", R.drawable.title_new_world, null);

	private static final Map<String, MapSize> PRODUCT_ID_MAP = new HashMap<>();
	static {
		for (MapSize size : MapSize.values()) {
			PRODUCT_ID_MAP.put(size.productId, size);
		}
	}
	public static MapSize getMapSizeByProductId(String id) {
		return PRODUCT_ID_MAP.get(id);
	}
	
	public final CatanMapProvider mapProvider;
	public final String title;
	public final int titleDrawableId;
	public final String productId;
	
	MapSize(CatanMapProvider mapProvider, String title, int titleDrawableId, String productId) {
		this.mapProvider = mapProvider;
		this.title = title;
		this.titleDrawableId = titleDrawableId;
		this.productId = productId;
	}

    public static final Parcelable.Creator<MapSize> CREATOR = new Parcelable.Creator<MapSize>() {
        public MapSize createFromParcel(Parcel in) {
            return MapSize.values()[in.readInt()];
        }

        public MapSize[] newArray(int size) {
            return new MapSize[size];
        }
    };
	
	@Override
	public int describeContents() {
        return 0;
    }

	@Override
    public void writeToParcel(Parcel out, int flags) {
		out.writeInt(ordinal());
    }
	
	@Override
	public String toString() {
		return title;
	}
} public final class MapType {
	private MapType() {}
	public static final int TRADITIONAL = 0;
	public static final int FAIR = 1;
	public static final int RANDOM = 2;
	
	public static String getString(int mapType) {
		switch (mapType) {
		case TRADITIONAL:
			return "TRADITIONAL";
		case FAIR:
			return "FAIR";
		case RANDOM:
			return "RANDOM";
		default:
			return "UNKNOWN";
		}
	}
} public enum Resource implements Parcelable {
	DESERT(0xFFf0dc82, NumberOfResource.DESERT, "desert"),
	WHEAT(0xFFfad111, NumberOfResource.HIGH, "wheat"),
	CLAY(0xFFb22222, NumberOfResource.LOW, "clay"),
	ROCK(0xFF9e9e9e, NumberOfResource.LOW, "rock"),
	SHEEP(0xFF66ce5f, NumberOfResource.HIGH, "sheep"),
	WOOD(0xFF0c9302, NumberOfResource.HIGH, "wood"),
	WATER(0xFF00aeef, NumberOfResource.WATER, "water"),
	GOLD(0xFFAF7817, NumberOfResource.GOLD, "gold");

	private static final Map<String, Resource> JSON_KEY_MAP = new HashMap<>();
	static {
		for (Resource resource : Resource.values()) {
			JSON_KEY_MAP.put(resource.jsonKey, resource);
		}
	}
	public static Resource getResourceByJson(String key) {
		return JSON_KEY_MAP.get(key);
	}
	
	public final int color;
	public final int numOfResource;
	public final String jsonKey;
	
	Resource(int color, int numOfResource, String jsonKey) {
		this.color = color;
		this.numOfResource = numOfResource;
		this.jsonKey = jsonKey;
	}

    public static final Parcelable.Creator<Resource> CREATOR = new Parcelable.Creator<Resource>() {
        public Resource createFromParcel(Parcel in) {
            return Resource.values()[in.readInt()];
        }

        public Resource[] newArray(int size) {
            return new Resource[size];
        }
    };
	
	@Override
	public int describeContents() {
        return 0;
    }

	@Override
    public void writeToParcel(Parcel out, int flags) {
		out.writeInt(ordinal());
    }
	
	@Override
	public String toString() {
		return jsonKey;
	}
} public class MapsDialogFragment extends DialogFragment {
	private static final String SHARED_PREFS_NAME = "Maps";
	private static final String SHARED_PREFS_SHOWN_HELP = "NoSeafarers";
	
	public static MapsDialogFragment newInstance() {
		MapsDialogFragment f = new MapsDialogFragment();
		f.setStyle(STYLE_NO_TITLE, 0);
		return f;
	}
	
	@Override
	public void onActivityCreated (Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);

        Analytics.trackView(getActivity(), Analytics.VIEW_MAPS_MENU);
	}
	
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		View view = inflater.inflate(R.layout.maps, container, false);

		ImageView settlersButton = (ImageView) view.findViewById(R.id.settlers_item);
		settlersButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
			    ft.addToBackStack(null);
			    
				SettlersDialogFragment.newInstance().show(ft, "SettlersDialogFragment");
                Analytics.track(getActivity(), Analytics.CATEGORY_MAPS_MENU,
                        Analytics.ACTION_BUTTON, Analytics.SEE_SETTLERS);
			}
		});
		
		maybeShowSeafarers(view.findViewById(R.id.seafarers_item),
				(MapFragment) getActivity().getSupportFragmentManager().findFragmentById(R.id.map_fragment));
		
		ImageView moreButton = (ImageView) view.findViewById(R.id.more_item);
		moreButton.setOnClickListener(new OnClickListener() {
			@Override
			public void onClick(View v) {
				FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
			    ft.addToBackStack(null);
			    
				MenuDialogFragment.newInstance().show(ft, "MenuDialogFragment");
                Analytics.track(getActivity(), Analytics.CATEGORY_MAPS_MENU,
                        Analytics.ACTION_BUTTON, Analytics.SEE_MORE);
			}
		});

		getDialog().getWindow().getAttributes().windowAnimations = R.style.FadeDialogAnimation;
		
		return view;
	}
	
	private void maybeShowSeafarers(View seafarersButton, MapFragment mapFragment) {
		if (mapFragment.getShowSeafarers()) {
			seafarersButton.setVisibility(View.VISIBLE);
			seafarersButton.setOnClickListener(new OnClickListener() {
				@Override
				public void onClick(View v) {
					FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
				    ft.addToBackStack(null);
				    
					SeafarersDialogFragment.newInstance().show(ft, "SeafarersDialogFragment");
                    Analytics.track(getActivity(), Analytics.CATEGORY_MAPS_MENU,
                            Analytics.ACTION_BUTTON, Analytics.SEE_SEAFARERS);
				}
			});
		} else {
			SharedPreferences prefs = getActivity().getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
			boolean shownWhatsNew = prefs.getBoolean(SHARED_PREFS_SHOWN_HELP, false);
			if (!shownWhatsNew) {
				FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
				NoSeafarersDialogFragment.newInstance().show(ft, "NoSeafarersDialogFragment");
				SharedPreferences.Editor prefsEditor = prefs.edit();
				prefsEditor.putBoolean(SHARED_PREFS_SHOWN_HELP, true);
				prefsEditor.apply();
			}
			
			seafarersButton.setVisibility(View.GONE);
		}
	}
} public class WelcomeDialogFragment extends DialogFragment {
	public static WelcomeDialogFragment newInstance() {
		return new WelcomeDialogFragment();
	}
	
	@Override
	public Dialog onCreateDialog(Bundle savedInstanceState) {
		return new AlertDialog.Builder(getActivity())
			.setIcon(R.drawable.icon)
			.setTitle("Better Settlers Board Generator")
			.setMessage("Designed to make game setup for Settlers of Catan faster "
				+ "and simpler, the generator creates gameplay that is more evenly "
				+ "challenging and engaging. The result is a better game of Settlers.\n\n"
				+ "What's new in this version:\n"
				+ "- User interface cleanup\n"
				+ "- Performance improvements\n"
				+ "- Smaller app size\n"
				+ "- Bug fixes\n")
				.setPositiveButton("About", new OnClickListener() {
					@Override
					public void onClick(DialogInterface dialog, int which) {
						(new AboutDialogFragment()).show(getFragmentManager(), "AboutDialogFragment");
					}
				})
				.setNegativeButton("Dismiss", new OnClickListener() {
					public void onClick(DialogInterface dialog, int id) {
						dialog.dismiss();
					}
				})
			.create();
	}
} public class MapLogic {
	// Prevent instantiation
	private MapLogic() {}
	
	private static final Random RAND = new Random();
	
	public static ArrayList<Integer> getProbabilities(CatanMap currentMap, int currentType, ArrayList<Resource> resources) {
		switch(currentType) {
			case MapType.TRADITIONAL:
				return null; // Doesn't make any sense
			case MapType.FAIR:
				return getOrderedProbabilities(currentMap, resources);
			case MapType.RANDOM:
				return getRandomProbabilities(currentMap, resources);
			default:
				return null;
		}
	}
	
	public static ArrayList<Integer> getProbabilities(CatanMap currentMap, int currentType) {
		switch(currentType) {
			case MapType.TRADITIONAL:
				return getTraditionalProbabilities(currentMap);
			case MapType.FAIR:
				return getOrderedProbabilities(currentMap);
			case MapType.RANDOM:
				return getRandomProbabilities(currentMap);
			default:
				return null;
		}
	}
	
	private static ArrayList<Integer> getTraditionalProbabilities(CatanMap currentMap) {
		ArrayList<Integer> avail = initOrderedProbabilitiesNoZeros(currentMap);
		
		// Insert random desert locations
		while (avail.size() < currentMap.landGrid.length) {
			avail.add(RAND.nextInt(avail.size()), 0);
		}
		
		// Map it to the left to right top to bottom we expect
		int[] set = new int[avail.size()];
		for (int i : currentMap.landGridOrder) {
			set[i] = avail.remove(0);
		}
		
		ArrayList<Integer> setList = new ArrayList<>();
		for (int i : set) {
			setList.add(i);
		}

		return setList;
	}
	
	private static ArrayList<Integer> getRandomProbabilities(CatanMap currentMap) {
		ArrayList<Integer> set = new ArrayList<>();
		ArrayList<Integer> avail = initProbabilities(currentMap);
		while (!avail.isEmpty()) {
			// If we have an assigned prob, use it
			if (currentMap.landGridProbabilities[set.size()] != Integer.MAX_VALUE) {
				//BetterLog.v(" pulling special " + counter);
				int nextProb = currentMap.landGridProbabilities[set.size()];
				avail.remove((Integer) nextProb);
				set.add(nextProb);
			} else {
				set.add(avail.remove(RAND.nextInt(avail.size())));
			}
		}
		return set;
	}
	
	public static ArrayList<Integer> getUnknownProbabilities(CatanMap currentMap) {
		ArrayList<Integer> set = new ArrayList<>();
		ArrayList<Integer> avail = new ArrayList<>();
		for (int i : currentMap.availableUnknownProbabilities) {
			avail.add(i);
		}
		
		while (!avail.isEmpty()) {
			set.add(avail.remove(RAND.nextInt(avail.size())));
		}
		return set;
	}
	
	private static ArrayList<Integer> getRandomProbabilities(CatanMap currentMap, ArrayList<Resource> resources) {
		ArrayList<Integer> set = new ArrayList<>();
		ArrayList<Integer> avail = initProbabilitiesNoZeros(currentMap);
		int counter = 0;
		while (!avail.isEmpty()) {
			// If we have an assigned prob, use it
			if (currentMap.landGridProbabilities[counter] != Integer.MAX_VALUE) {
				//BetterLog.v(" pulling special " + counter);
				int nextProb = currentMap.landGridProbabilities[counter];
				avail.remove((Integer) nextProb);
				set.add(nextProb);
			} else {
				if (resources.get(counter) == Resource.DESERT) {
					set.add(0);
				} else {
					set.add(avail.remove(RAND.nextInt(avail.size())));
				}
			}
			//BetterLog.v("Check " + counter + ", set " + set.get(counter));
			counter++;
		}
		
		// Check for deserts at the end
		while (set.size() < resources.size()) {
			set.add(0);
		}
		
		return set;
	}
	
	private static ArrayList<Integer> getOrderedProbabilities(CatanMap currentMap) {
		// Contains the probabilities already set
		ArrayList<Integer> set = new ArrayList<>();
		// Contains the probabilities already consumed in this iteration
		ArrayList<Integer> tried = new ArrayList<>();
		// Contains the probabilities yet to be consumed
		ArrayList<Integer> avail = initProbabilities(currentMap);
		boolean placedRecently = false;
		
		while (!tried.isEmpty() || !avail.isEmpty()) {
			if (placedRecently && !tried.isEmpty()) {
				avail.addAll(tried);
				tried.clear();
				placedRecently = false;
			}
			// Game over: avail is empty and we still have un-set probabilities. Start over.
			if (avail.isEmpty()) {
				//BetterLog.v("Restart: " + set);
				set.clear();
				tried.clear();
				avail = initProbabilities(currentMap);
				placedRecently = false;
			} else {
				//BetterLog.v("  Set      : " + set);
				//BetterLog.v("  Tried    : " + tried);
				//BetterLog.v("  Avail    : " + avail);
				
				String whitelistName = currentMap.landGridWhitelists[set.size()];
				
				int nextProb;
				if (currentMap.landGridProbabilities[set.size()] != Integer.MAX_VALUE) {
					// If we have an assigned prob, use it
					nextProb = currentMap.landGridProbabilities[set.size()];
					avail.remove((Integer) nextProb);
					set.add(nextProb);
					placedRecently = true;
					continue;
				} else if (whitelistName != null
						&& currentMap.landProbabilityWhitelists.containsKey(whitelistName)) {
					// If we have a prob whitelist, pull one from it.
					// Grab a random one
					List<Integer> probChoices = currentMap.landProbabilityWhitelists.get(whitelistName);
					nextProb = -1;
					while (nextProb == -1) {
						int maybeNextProb = probChoices.get(RAND.nextInt(probChoices.size()));
						if (avail.contains(maybeNextProb)) {
							nextProb = maybeNextProb;
							avail.remove((Integer) maybeNextProb);
						}
					}
				} else {
					nextProb = avail.remove(RAND.nextInt(avail.size()));
				}

				//BetterLog.v("  Consuming: " + nextProb);
				boolean canPlaceHere = true;
				
				// If it's a desert (picking a 0), make sure the whitelist (if it has one) has desert
				if (nextProb == 0 && whitelistName != null
						&& currentMap.landResourceWhitelists.containsKey(whitelistName)
						&& !currentMap.landResourceWhitelists.get(whitelistName).contains(Resource.DESERT)) {
					canPlaceHere = false;
				}
				
				// Check all intersections that contain this piece
				for (int tripletIndex : currentMap.landIntersectionIndexes[set.size()]) {
					int[] triplet = currentMap.landIntersections[tripletIndex];
					if (triplet.length < 3) {
						continue; // Skip shoreline
					}
					
					ArrayList<Integer> tempTriplets = new ArrayList<>();
					tempTriplets.add(nextProb);
					boolean goAhead = true;
					for (int trip : triplet) {
						if (trip < set.size()) {
							int num = set.get(trip);
							if (num < 0) {
								goAhead = false; // Skip empty probs
								break;
							}
							tempTriplets.add(num);
						}
					}
					if (!goAhead) {
						continue;
					}
					
					if (!noDuplicates(currentMap, tempTriplets)) {
						canPlaceHere = false;
					} else if (tempTriplets.size() == 3) {
						if (tempTriplets.contains(0)) {
							// Ignore if it has more than one desert (only for customs)
							tempTriplets.remove((Integer) 0);
							if (!tempTriplets.contains(0)) {
								tempTriplets.add(0);
								// Has a desert has to be 4<=x<=8
								if (sumProbability(tempTriplets) < 4 || sumProbability(tempTriplets) > 8) {
									canPlaceHere = false;
								}
							}
						} else {
							if (sumProbability(tempTriplets) < 5 || sumProbability(tempTriplets) > 11) {
								canPlaceHere = false;
							}
						}
					}
				}
				if (canPlaceHere) {
					set.add(nextProb);
					placedRecently = true;
				} else {
					tried.add(nextProb);
				}				
			}
		}

		//BetterLog.v("Finished: " + set);
		return set;
	}
	
	private static ArrayList<Integer> getOrderedProbabilities(CatanMap currentMap, ArrayList<Resource> resources) {
		// Contains a mapping to which resources have what so far
		Map<Resource, ArrayList<Integer>> resourceMap = initResourceMap(currentMap);
		// Contains the resources already set
		ArrayList<Integer> set = new ArrayList<>();
		// Contains the resources already consumed in this iteration
		ArrayList<Integer> tried = new ArrayList<>();
		// Contains the (non desert) resources yet to be consumed
		ArrayList<Integer> avail = initProbabilitiesNoZeros(currentMap);
		boolean placedRecently = false;
		
		while (!tried.isEmpty() || !avail.isEmpty()) {
			if (placedRecently && !tried.isEmpty()) {
				avail.addAll(tried);
				tried.clear();
				placedRecently = false;
			}
			// Game over: avail is empty and we still have un-set probabilities. Start over.
			if (avail.isEmpty()) {
				//BetterLog.v("Restart: " + set);
				resourceMap = initResourceMap(currentMap);
				set.clear();
				tried.clear();
				avail = initProbabilitiesNoZeros(currentMap);
				placedRecently = false;
			} else {
				int nextIndex = set.size();
				Resource nextResource = resources.get(nextIndex);
				String whitelistName = currentMap.landGridWhitelists[set.size()];
				
				// Consume
				// Always place a 0 on a desert
				int nextProb;
				if (currentMap.landGridProbabilities[set.size()] != Integer.MAX_VALUE) {
					// If we have an assigned prob, use it
					nextProb = currentMap.landGridProbabilities[set.size()];
					avail.remove((Integer) nextProb);
					set.add(nextProb);
					placedRecently = true;
					continue;
				} else if (nextResource == Resource.DESERT) {
					set.add(0);
					placedRecently = true;
					continue;
				} else if (whitelistName != null
						&& currentMap.landProbabilityWhitelists.containsKey(whitelistName)) {
					// If we have a prob whitelist, pull one from it.
					// Grab a random one
					List<Integer> probChoices = currentMap.landProbabilityWhitelists.get(whitelistName);
					nextProb = -1;
					while (nextProb == -1) {
						int maybeNextProb = probChoices.get(RAND.nextInt(probChoices.size()));
						if (avail.contains(maybeNextProb)) {
							nextProb = maybeNextProb;
							avail.remove((Integer) maybeNextProb);
						}
					}
				} else {
					nextProb = avail.remove(RAND.nextInt(avail.size()));
				}
				
				//BetterLog.v("  Set: " + set);
				//BetterLog.v("  Avail: " + avail);
				//BetterLog.v("  Tried: " + tried);
				//BetterLog.v("  Consuming: " + nextResource);
				boolean canPlaceHere = true;
				
				// If it's a desert (picking a 0), make sure the whitelist (if it has one) has desert
				if (nextProb == 0 && whitelistName != null
						&& currentMap.landResourceWhitelists.containsKey(whitelistName)
						&& !currentMap.landResourceWhitelists.get(whitelistName).contains(Resource.DESERT)) {
					canPlaceHere = false;
				}

				// Check all intersections that contain this piece
				for (int tripletIndex : currentMap.landIntersectionIndexes[set.size()]) {
					int[] triplet = currentMap.landIntersections[tripletIndex];
					if (triplet.length < 3) {
						continue; // Skip shoreline
					}
					
					ArrayList<Integer> tempTriplets = new ArrayList<>();
					tempTriplets.add(nextProb);
					boolean goAhead = true;
					for (int trip : triplet) {
						if (trip < set.size()) {
							int num = set.get(trip);
							if (num < 0) {
								goAhead = false; // Skip empty probs
								break;
							}
							tempTriplets.add(num);
						}
					}
					if (!goAhead) {
						continue;
					}
					
					if (!noDuplicates(currentMap, tempTriplets)) {
						canPlaceHere = false;
					} else if (tempTriplets.size() == 3) {
						if (tempTriplets.contains(0)) {
							// Ignore if it has more than one desert (only for customs)
							tempTriplets.remove((Integer) 0);
							if (!tempTriplets.contains(0)) {
								tempTriplets.add(0);
								// Has a desert has to be 4<=x<=8
								if (sumProbability(tempTriplets) < 4 || sumProbability(tempTriplets) > 8) {
									canPlaceHere = false;
								}
							}
						} else {
							if (sumProbability(tempTriplets) < 5 || sumProbability(tempTriplets) > 11) {
								canPlaceHere = false;
							}
						}
					}
				}
				
				// If this number is already used by the same resource
				if (resourceMap.get(nextResource).contains(nextProb)) {
					canPlaceHere = false;
				}
				

				// If this is the last resource, check the probs of this resource
				int numOfResource = nextResource.numOfResource;
				ArrayList<Integer> tmpProbs = new ArrayList<>();
				tmpProbs.addAll(resourceMap.get(nextResource));
				tmpProbs.add(nextProb);
				if (numOfResource == NumberOfResource.LOW
						&& tmpProbs.size() == currentMap.lowResourceNumber) {
					if (sumProbability(tmpProbs) < 3*currentMap.lowResourceNumber
							|| sumProbability(tmpProbs) > 4*currentMap.lowResourceNumber) {
						canPlaceHere = false;
					}
					if (!isBalanced(tmpProbs)) {
						canPlaceHere = false;
					}
				} else if (numOfResource == NumberOfResource.HIGH
						&& tmpProbs.size() == currentMap.highResourceNumber) {
					if (sumProbability(tmpProbs) < 3*currentMap.highResourceNumber
							|| sumProbability(tmpProbs) > 4*currentMap.highResourceNumber) {
						canPlaceHere = false;
					}
				}
				
				if (canPlaceHere) {
					set.add(nextProb);
					resourceMap.get(nextResource).add(nextProb);
					placedRecently = true;
				} else {
					tried.add(nextProb);
				}				
			}
		}
		
		// Check for deserts at the end
		while (set.size() < resources.size()) {
			set.add(0);
		}

		//BetterLog.v("Finished: " + set);
		return set;
	}
	
	public static ArrayList<Resource> getResources(CatanMap currentMap, int currentType, ArrayList<Integer> probs) {
		switch(currentType) {
			case MapType.TRADITIONAL:
				// Normal actually uses random resource distribution (prob setup determines "normalness")
				return getRandomResources(currentMap, probs);
			case MapType.FAIR:
				return getOrderedResources(currentMap, probs);
			case MapType.RANDOM:
				return getRandomResources(currentMap, probs);
			default:
				return null;
		}
	}
	
	public static ArrayList<Resource> getUnknowns(CatanMap currentMap, ArrayList<Integer> probs) {
		ArrayList<Resource> set = new ArrayList<>();
		ArrayList<Resource> avail = new ArrayList<>();
		Collections.addAll(avail, currentMap.availableUnknownResources);
		
		while (!avail.isEmpty()) {
			int nextProb = probs.get(set.size());
			
			if (nextProb == 0) {
				avail.remove(Resource.DESERT);
				set.add(Resource.DESERT);
			} else if (nextProb == -1) {
				avail.remove(Resource.WATER);
				set.add(Resource.WATER);
			} else {
				Resource next = avail.remove(RAND.nextInt(avail.size()));
				if (next == Resource.DESERT || next == Resource.WATER){
					avail.add(next); // put it back
				} else {
					set.add(next);
				}
			}
		}
		
		return set;
	}
	
	private static ArrayList<Resource> getRandomResources(CatanMap currentMap, ArrayList<Integer> probs) {
		ArrayList<Resource> set = new ArrayList<>();
		ArrayList<Resource> avail = initAvailResourcesNoDesert(currentMap);
		Map<String, List<Resource>> allowedWhitelists = getAllowedWhitelists(currentMap);
		
		while (!avail.isEmpty()) {
			int nextIndex = set.size();
			int nextProb = probs.get(nextIndex);

			String whitelistName = currentMap.landGridWhitelists[nextIndex];
			
			// Consume
			// Always place a random resource on a -1
			// Always place a desert on a 0
			Resource nextResource;
			if (nextProb == 0) {
				set.add(Resource.DESERT);
			} else if (nextProb < -1 && whitelistName != null && allowedWhitelists.containsKey(whitelistName)
					&& allowedWhitelists.get(whitelistName).contains(Resource.DESERT)) {
				// Special case deserts with probs
				set.add(Resource.DESERT);
			} else if (currentMap.landGridResources[nextIndex] != null) {
				nextResource = currentMap.landGridResources[nextIndex];
				// If we have an assigned resource, use it
				set.add(nextResource);
				avail.remove(nextResource);
			} else {
				set.add(avail.remove(RAND.nextInt(avail.size())));
			}
		}
		
		// Check for deserts at the end
		while (set.size() < probs.size()) {
			set.add(Resource.DESERT);
		}
		
		return set;
	}
	
	private static ArrayList<Resource> getOrderedResources(CatanMap currentMap, ArrayList<Integer> probs) {
		// Contains a mapping to which resources have what so far
		Map<Resource, ArrayList<Integer>> resourceMap = initResourceMap(currentMap);
		// Contains the resources already set
		ArrayList<Resource> set = new ArrayList<>();
		// Contains the resources already consumed in this iteration
		ArrayList<Resource> tried = new ArrayList<>();
		// Contains the (non desert) resources yet to be consumed
		ArrayList<Resource> avail = initAvailResourcesNoDesert(currentMap);
		boolean placedRecently = false;
		
		Map<String, List<Resource>> allowedWhitelists = getAllowedWhitelists(currentMap);
		
		while (!tried.isEmpty() || !avail.isEmpty()) {
			if (placedRecently && !tried.isEmpty()) {
				avail.addAll(tried);
				tried.clear();
				placedRecently = false;
			}
			// Game over: avail is empty and we still have un-set resources. Start over.
			if (avail.isEmpty()) {
				//BetterLog.v("Restart");
				//BetterLog.v("  set (" + set.size() + "): " + set);
				//BetterLog.v("  probs: " + probs);
				resourceMap = initResourceMap(currentMap);
				set.clear();
				tried.clear();
				avail = initAvailResourcesNoDesert(currentMap);
				placedRecently = false;
				allowedWhitelists = getAllowedWhitelists(currentMap);
			} else {
				//BetterLog.v("Set: " + set);
				//BetterLog.v("Avail: " + avail);
				//BetterLog.v("Tried: " + tried);
				//BetterLog.v("Probs: " + probs);
				
				int nextIndex = set.size();
				int nextProb = probs.get(nextIndex);

				String whitelistName = currentMap.landGridWhitelists[nextIndex];
				
				// Consume
				// Always place a random resource on a -1
				// Always place a desert on a 0
				Resource nextResource;
				if (nextProb == 0) {
					set.add(Resource.DESERT);
					placedRecently = true;
					continue;
				} else if (nextProb < -1 && whitelistName != null && allowedWhitelists.containsKey(whitelistName)
						&& allowedWhitelists.get(whitelistName).contains(Resource.DESERT)) {
					// Special case deserts with probs
					set.add(Resource.DESERT);
					placedRecently = true;
					continue;
				} else if (currentMap.landGridResources[nextIndex] != null) {
					nextResource = currentMap.landGridResources[nextIndex];
					// If we have an assigned resource, use it
					set.add(nextResource);
					resourceMap.get(nextResource).add(nextProb);
					avail.remove(nextResource);
					placedRecently = true;
					continue;
				} else {
					nextResource = avail.remove(RAND.nextInt(avail.size()));
				}
				
				//BetterLog.v("Consuming: " + nextResource);
				boolean canPlaceHere = true;
				
				// Check to see if there's a whitelist
				if (whitelistName != null && allowedWhitelists.containsKey(whitelistName)
						&& !allowedWhitelists.get(whitelistName).contains(nextResource)) {
					canPlaceHere = false;
				}
				
				if (nextProb == -1 && canPlaceHere) { // Just use it
					set.add(nextResource);
					resourceMap.get(nextResource).add(nextProb);
					placedRecently = true;
					if (whitelistName != null && allowedWhitelists.containsKey(whitelistName)) {
						allowedWhitelists.get(whitelistName).remove(nextResource);
					}
					continue;
				}
				
				// Check neighbors for same resource.
				if (!currentMap.name.equals("the_pirate_islands")
						&& !currentMap.name.equals("the_pirate_islands_exp")) {
					for (int neighbor : currentMap.landNeighbors[nextIndex]) {
						//BetterLog.v("    " + neighbor);
						if (neighbor >= set.size()) {
							// Do nothing, it is not yet occupied
						} else {
							if (set.get(neighbor) == nextResource) {
								canPlaceHere = false;
								break;
							} else {
								// Do nothing, at least this neighbor isn't the same
							}
						}
					}
				}
				
				// If this number is already used by the same resource
				if (resourceMap.get(nextResource).contains(nextProb)
						&& !currentMap.name.equals("the_pirate_islands")
						&& !currentMap.name.equals("the_pirate_islands_exp")) {
					canPlaceHere = false;
				}
				
				// No 6s or 8s on golds
				if (nextResource == Resource.GOLD && (probs.get(nextIndex) == 6 || probs.get(nextIndex) == 8)) {
					canPlaceHere = false;
				}
				

				// If this is the last resource, check the probs of this resource
				int numOfResource = nextResource.numOfResource;
				ArrayList<Integer> tmpProbs = new ArrayList<>();
				tmpProbs.addAll(resourceMap.get(nextResource));
				tmpProbs.add(nextProb);
				if (!tmpProbs.contains(-1)) {
					// Skip missing probs
					if (numOfResource == NumberOfResource.LOW
							&& tmpProbs.size() == currentMap.lowResourceNumber) {
						if (sumProbability(tmpProbs) < 3*currentMap.lowResourceNumber
								|| sumProbability(tmpProbs) > 4*currentMap.lowResourceNumber) {
							canPlaceHere = false;
						}
						if (!isBalanced(tmpProbs)) {
							canPlaceHere = false;
						}
					} else if (numOfResource == NumberOfResource.HIGH
							&& tmpProbs.size() == currentMap.highResourceNumber) {
						if (sumProbability(tmpProbs) < 3*currentMap.highResourceNumber
								|| sumProbability(tmpProbs) > 4*currentMap.highResourceNumber) {
							canPlaceHere = false;
						}
					}
				}
				
				if (canPlaceHere) {
					set.add(nextResource);
					resourceMap.get(nextResource).add(nextProb);
					placedRecently = true;
					if (whitelistName != null && allowedWhitelists.containsKey(whitelistName)) {
						allowedWhitelists.get(whitelistName).remove(nextResource);
					}
				} else {
					tried.add(nextResource);
				}				
			}
		}
		
		// Check for deserts at the end
		while (set.size() < probs.size()) {
			set.add(Resource.DESERT);
		}

		//BetterLog.v("Finished: " + set);
		return set;
	}
	
	public static ArrayList<Harbor> getHarbors(CatanMap currentMap, int currentType,
			ArrayList<Resource> resourceList,	ArrayList<Integer> numberList) {
		switch(currentType) {
			case MapType.TRADITIONAL:
				return getTraditionalHarbors(currentMap);
			case MapType.FAIR:
				//return getRandomHarbors(currentMap);
				return getOrderedHarbors(currentMap, resourceList, numberList);
			case MapType.RANDOM:
				return getRandomHarbors(currentMap);
			default:
				return null;
		}
	}

	/**
	 * These have to be hardcoded since they're so ugly
	 */
	private static ArrayList<Harbor> getTraditionalHarbors(CatanMap currentMap) {
		ArrayList<Resource> toTake = initHarbors(currentMap);
		ArrayList<Harbor> harbors = new ArrayList<>();

		int i = 0;
		for (int dir : currentMap.orderedHarbors) {
			if (dir == -1) { // Empty water
				harbors.add(new Harbor(i, Resource.WATER, currentMap.waterNeighbors[i][0]));
			} else {
				harbors.add(new Harbor(i, toTake.remove(RAND.nextInt(toTake.size())), currentMap.waterNeighbors[i][dir]));
			}
			i++;
		}
    
    return harbors;
	}
	
	private static ArrayList<Harbor> getRandomHarbors(CatanMap currentMap) {
		ArrayList<Resource> toTake = initHarbors(currentMap);
		ArrayList<Harbor> harbors = new ArrayList<>();
		
		// First fill with water
		for (int i = 0; i < currentMap.waterGrid.length; i++) {
			harbors.add(new Harbor(i, Resource.WATER, -1));      
		}
		
		while (!toTake.isEmpty()) {
			// Make sure that we're not overwriting a good harbor already placed
			int rand = RAND.nextInt(currentMap.waterGrid.length);
			while (harbors.get(rand).resource != Resource.WATER) {
				rand = RAND.nextInt(currentMap.waterGrid.length);
			}
			
			// If no land, skip it
			if (currentMap.waterNeighbors[rand] == null) {
				continue;
			}

			int currentHarborPosition = harbors.get(rand).position;
			Resource resource = toTake.remove(0);
			int facing;
			if (currentMap.waterNeighbors[rand].length > 1) {
				if (isCustom(currentMap)) {
					facing = currentMap.waterNeighbors[rand][0];
				} else {
					facing = currentMap.waterNeighbors[rand][RAND.nextInt(2)];
				}
			} else {
				facing = currentMap.waterNeighbors[rand][0];
			}
			harbors.set(rand, new Harbor(currentHarborPosition, resource, facing));
		}
		return harbors;
	}

	/**
	 * Given the current resource/probabilities map of Catan, figure out
	 * fair harbors and return a list of them in order. Basic algorithm is
	 * to dole out random harbors and see if it's fair.  If not, try again.
	 */
	private static ArrayList<Harbor> getOrderedHarbors(CatanMap currentMap, ArrayList<Resource> resourceList,
			ArrayList<Integer> numberList) {
		ArrayList<Resource> resources;
		ArrayList<Harbor> harbors;

		while (true) {
			int pos = 0;
			harbors = new ArrayList<>();
			resources = initHarbors(currentMap);

			// Quick coin-flip to see to start with open ocean or not
			boolean coinFlip = RAND.nextBoolean();
			while (harbors.size() < currentMap.waterGrid.length) {
				// If we're out of harbors, just fill the rest in with water
				if (resources.isEmpty()) {
					harbors.add(new Harbor(pos, Resource.WATER, -1));
					pos++;
					continue;
				}
				
				int nextResourceIndex = RAND.nextInt(resources.size());
				if (coinFlip) {
					if (currentMap.name.equals("new_world") || currentMap.name.equals("new_world_exp")) {
						boolean skip = false;
						while (neighborConflict(currentMap, harbors, pos)) {
							harbors.add(new Harbor(pos, Resource.WATER, -1));
							pos++;
							if (pos >= currentMap.waterGrid.length) {
								skip = true;
								break;
							}
						}
						if (skip) {
							break;
						}
					}
					
					// no land around it
					if (currentMap.waterNeighbors[pos] == null) {
						harbors.add(new Harbor(pos, Resource.WATER, -1));
					} else {
						if (currentMap.waterNeighbors[pos].length > 1) {
							if (isCustom(currentMap)) {
								harbors.add(new Harbor(pos, resources.remove(nextResourceIndex),
										currentMap.waterNeighbors[pos][0]));
							} else {
								harbors.add(new Harbor(pos, resources.remove(nextResourceIndex),
										currentMap.waterNeighbors[pos][RAND.nextInt(2)]));
							}
						} else {
							harbors.add(new Harbor(pos, resources.remove(nextResourceIndex),
									currentMap.waterNeighbors[pos][0]));
						}
					}
					pos++;

					if (harbors.size() >= currentMap.waterGrid.length) {
						break;
					}

					// no land around it
					if (currentMap.waterNeighbors[pos] == null) {
						harbors.add(new Harbor(pos, Resource.WATER, -1));
					} else {
						harbors.add(new Harbor(pos, Resource.WATER, currentMap.waterNeighbors[pos][0]));
					}
					pos++;
				} else {
					// no land around it
					if (currentMap.waterNeighbors[pos] == null) {
						harbors.add(new Harbor(pos, Resource.WATER, -1));
					} else {
						harbors.add(new Harbor(pos, Resource.WATER, currentMap.waterNeighbors[pos][0]));
					}
					pos++;

					if (harbors.size() >= currentMap.waterGrid.length) {
						break;
					}
					
					if (currentMap.name.equals("new_world") || currentMap.name.equals("new_world_exp")) {
						boolean skip = false;
						while (neighborConflict(currentMap, harbors, pos)) {
							harbors.add(new Harbor(pos, Resource.WATER, -1));
							pos++;
							if (pos >= currentMap.waterGrid.length) {
								skip = true;
								break;
							}
						}
						if (skip) {
							break;
						}
					}

					// no land around it
					if (currentMap.waterNeighbors[pos] == null) {
						harbors.add(new Harbor(pos, Resource.WATER, -1));
					} else {
						if (currentMap.waterNeighbors[pos].length > 1) {
							if (isCustom(currentMap)) {
								harbors.add(new Harbor(pos, resources.remove(nextResourceIndex),
										currentMap.waterNeighbors[pos][0]));
							} else {
								harbors.add(new Harbor(pos, resources.remove(nextResourceIndex),
										currentMap.waterNeighbors[pos][RAND.nextInt(2)]));
							}
						} else {
							harbors.add(new Harbor(pos, resources.remove(nextResourceIndex),
									currentMap.waterNeighbors[pos][0]));
						}
					}
					pos++;
				}
			}

			// Check to see if things are fair. A 2:1 harbor can't touch one of it's own terrain
			// tiles of probability 5, 6, 8, or 9.
			boolean goAhead = true;
			for (int i = 0; i < harbors.size(); i++) {
				Harbor harbor = harbors.get(i);
				Resource harborResource = harbor.resource;

				if (harborResource != Resource.DESERT && harborResource != Resource.WATER) {
					int[] neighbors = currentMap.waterNeighbors[i];
					for (int neighbor : neighbors) {
						Resource landResource = resourceList.get(neighbor);
						int landNumber = numberList.get(neighbor);
						if (harborResource == landResource && (landNumber >= 5 && landNumber <= 9)) {
							//BetterLog.v("    Skipping " + harborResource + ", " + landNumber);
							goAhead = false;
							break;
						} else {
							//BetterLog.v("NOT SKipping " + harborResource + ", " + landNumber);
						}
					}
				}
			}
			if (goAhead) {
				break;
			}
		}

		return harbors;
	}
	
	/**
	 * Helper for getOrderdHarbors for New World
	 */
	private static boolean neighborConflict(CatanMap currentMap, ArrayList<Harbor> harbors, int position) {
		Set<Integer> seenNeighbors = new HashSet<>();
		for (int i = 0; i < harbors.size(); i++) {
			if (harbors.get(i).resource != Resource.WATER && currentMap.waterNeighbors[i] != null) {
				seenNeighbors.add(harbors.get(i).position);
				for (int neighbor : currentMap.waterWaterNeighbors[i]) {
					seenNeighbors.add(neighbor);
				}
			}
		}

		// Check neighbors conflicting with neighbors
		if (currentMap.waterWaterNeighbors[position] != null) {
			for (int positionNeighbor : currentMap.waterWaterNeighbors[position]) {
				if (seenNeighbors.contains(positionNeighbor)) {
					return true;
				}
			}
		}
		// Also check itself
		return seenNeighbors.contains(position);
	}

	/**
	 * Private helper function that takes in a harbor and returns the
	 * orientation of its two arms facing inland.  In other words, 0
	 * if it is facing "left" (according to the harbor facing inward
	 * to the land tiles) or 1 for "right".  Or 0 if it has no choice.
	 * Used by the paint function.
	 */
	public static int whichWayHarborFaces(CatanMap currentMap, Harbor harbor) {
		int pos = harbor.position;
		
		if (currentMap.waterNeighbors[pos] == null) {
			return -1;
		}
		
		int len = currentMap.waterNeighbors[pos].length;
		if (len == 1) {
			return 0;
		} else { // len = 2
			if (harbor.facing == currentMap.waterNeighbors[pos][0]) {
				return 0;
			} else {
				return 1;
			}
		}
	}
	
	/**
	 * Helper function for getOrderedResources
	 */
	private static Map<String, List<Resource>> getAllowedWhitelists(CatanMap currentMap) {
		Map<String, List<Resource>> allowedWhitelists = new HashMap<>();
		if (currentMap.landResourceWhitelists != null) {
			for (Map.Entry<String, List<Resource>> entry : currentMap.landResourceWhitelists.entrySet()) {
				allowedWhitelists.put(entry.getKey(), new ArrayList<Resource>());
				for (Resource resource : entry.getValue()) {
					allowedWhitelists.get(entry.getKey()).add(resource);
				}
			}
		}
		return allowedWhitelists;
	}
	
	/**
	 * Helper function for getOrderedResources
	 */
	private static Map<Resource, ArrayList<Integer>> initResourceMap(CatanMap currentMap) {
		Map<Resource, ArrayList<Integer>> resourceMap = new HashMap<>();
		for (Resource resource : currentMap.availableResources) {
			resourceMap.put(resource, new ArrayList<Integer>());
		}
		return resourceMap;
	}
		
	/**
	 * Helper function for getOrderedResources
	 */
	private static ArrayList<Resource> initAvailResourcesNoDesert(CatanMap currentMap) {
		ArrayList<Resource> avail = new ArrayList<>();
		for (Resource resource : currentMap.availableResources) {
			if (resource != Resource.DESERT) {
				avail.add(resource);
			}
		}
		return avail;
	}
	
	/**
	 * Add in each of the probability pieces into an array and return it.
	 * Helper function for getOrderedProbabilities()
	 */
	private static ArrayList<Integer> initOrderedProbabilitiesNoZeros(CatanMap currentMap) {
		ArrayList<Integer> numbers = new ArrayList<>();
		for (int i : currentMap.availableOrderedProbabilities) {
			numbers.add(i);
		}
		return numbers;
	}
	
	/**
	 * Add in each of the probability pieces into an array and return it.
	 * Helper function for getOrderedProbabilities()
	 */
	private static ArrayList<Integer> initProbabilities(CatanMap currentMap) {
		ArrayList<Integer> numbers = new ArrayList<>();
		for (int i : currentMap.availableProbabilities) {
			numbers.add(i);
		}
		return numbers;
	}
	
	/**
	 * Add in each of the probability pieces into an array and return it.
	 * Helper function for getOrderedProbabilities()
	 */
	private static ArrayList<Integer> initProbabilitiesNoZeros(CatanMap currentMap) {
		ArrayList<Integer> numbers = new ArrayList<>();
		for (int i : currentMap.availableProbabilities) {
			if (i != 0) {
				numbers.add(i);
			}
		}
		return numbers;
	}

	/**
	 * Make sure that the given array list contains no duplicates.  Returns
	 * true for no duplicates; false otherwise.
	 */
	private static boolean noDuplicates(CatanMap currentMap, ArrayList<Integer> numbers) {
		for (int i = 0; i < numbers.size(); i++) {
			int num = numbers.remove(i);
			if (numbers.contains(num)
					&& !(currentMap.name.equals("through_the_desert") && num == 0)
					&& !(currentMap.name.equals("through_the_desert_exp") && num == 0)
					&& !(currentMap.name.equals("the_wonders_of_catan") && num == 0)
					&& !(currentMap.name.equals("the_wonders_of_catan_exp") && num == 0)) {
				// Be sure to put back at the proper place in the array so
				// that we actually go through all elements
				numbers.add(i, num);
				return false;
			} else {
				numbers.add(i, num);
			}
		}
		return true;
	}

	/**
	 * Make sure that within the given array list, no one tile has more than
	 * half the probability (is not more than half the sum of the rest).
	 */
	private static boolean isBalanced(ArrayList<Integer> numbers) {
		for (int i = 0; i < numbers.size(); i++) {
			int currentNumber = numbers.remove(i);
			if (PROBABILITY_MAPPING[currentNumber]
			                                 > sumProbability(numbers)) {
				return false;
			}
			numbers.add(i, currentNumber);
		}
		return true;
	}
	
	/**
	 * Sums up the integers in the array list and returns it the sum.
	 */
	private static int sumProbability(ArrayList<Integer> numbers) {
		int sum = 0;
		for (int number : numbers) {
			sum += PROBABILITY_MAPPING[number];
		}
		return sum;
	}
	
	/**
	 * Add in each of the harbor pieces into an array and return it.
	 * Helper function for getHarbors()
	 */
	private static ArrayList<Resource> initHarbors(CatanMap currentMap) {
		ArrayList<Resource> harbors = new ArrayList<>();
		Collections.addAll(harbors, currentMap.availableHarbors);
		return harbors;
	}
	
	private static boolean isCustom(CatanMap currentMap) {
		Set<String> nonCustomNames = new HashSet<>();
		nonCustomNames.add("standard");
		nonCustomNames.add("large");
		nonCustomNames.add("xlarge");
		return !nonCustomNames.contains(currentMap.name);
	}
} public final class PlacementLogic {
	// Prevent instantiation
	private PlacementLogic() {}

	private static int removeMax(SparseIntArray sums) {
		int maxIndex = 0;
		for (int i = 1; i < sums.size(); i++) {
			if (sums.valueAt(i) > sums.valueAt(maxIndex)) {
				maxIndex = i;
			}
		}
		int ret = sums.keyAt(maxIndex);
		sums.removeAt(maxIndex);
		return ret;
	}

	private static void addValue(SparseIntArray array, int key, int more) {
		array.put(key, array.get(key) + more);
	}

	/**
	 * Overall function returning the aggregate of all "best" placements.
	 * 
	 * @param currentMap Size of the map
	 * @param resources The list of ordered resources.
	 * @param probs The list of ordered probabilities.
	 * @param harbors The list of ordered harbors.
	 * @return A list of best intersections
	 */	
	public static Pair<ArrayList<Integer>, SparseArray<ArrayList<String>>> getBestPlacements(CatanMap currentMap,
			ArrayList<Resource> resources, ArrayList<Integer> probs, ArrayList<Harbor> harbors) {
		ArrayList<Integer> order = new ArrayList<>();
		SparseArray<ArrayList<String>> set = new SparseArray<>();
		SparseIntArray sums = new SparseIntArray();

		// 5pts x # of resources
		SparseIntArray highProbs = getHighProbabilities(currentMap, 0, probs, false);
		for (int i = 0; i < highProbs.size(); i++) {
			addValue(sums, highProbs.keyAt(i), 5 * highProbs.valueAt(i));
		}

		// 2 pts 25-50%, 4 pts 50-75%, 8 pts 75+%
		SparseIntArray rareResources = getRareResources(currentMap, 50, probs, resources);
		for (int i = 0; i < rareResources.size(); i++) {
			int key = rareResources.keyAt(i);
			int value = rareResources.valueAt(i);
			
			if (value > 25 && value < 50) {
				addValue(sums, key, 2);
			} else if (value > 50 && value < 75) {
				addValue(sums, key, 4);
			} else {
				addValue(sums, key, 8);
			}
		}

		// 1 pt x resource for a factory
		SparseIntArray factories = getFactoryPlacements(currentMap, 0, probs, resources, harbors);
		for (int i = 0; i < factories.size(); i++) {
			addValue(sums, factories.keyAt(i), factories.valueAt(i));
		}

		// 3 pts for a trader
		SparseIntArray traders = getTraderPlacements(currentMap, 5, probs, resources, harbors);
		for (int i = 0; i < traders.size(); i++) {
			addValue(sums, traders.keyAt(i), 4);
		}

		// 1 pt x resource for a road
		SparseIntArray roads = getRoadBuilder(currentMap, 3, probs, resources);
		for (int i = 0; i < roads.size(); i++) {
			addValue(sums, roads.keyAt(i), roads.valueAt(i));
		}

		// 1 pt x resource for a city
		SparseIntArray cities = getCityBuilder(currentMap, 3, probs, resources);
		for (int i = 0; i < cities.size(); i++) {
			addValue(sums, cities.keyAt(i), cities.valueAt(i));
		}

		// 1/2 pt x resource for a dev card
		SparseIntArray devCards = getDevCardBuilder(currentMap, 3, probs, resources);
		for (int i = 0; i < devCards.size(); i++) {
			addValue(sums, devCards.keyAt(i), devCards.valueAt(i) / 2);
		}

		// 3 pts for a ninja spot
		SparseIntArray ninjas = getHighProbabilities(currentMap, 10, probs, true);
		for (int i = 0; i < ninjas.size(); i++) {
			addValue(sums, ninjas.keyAt(i), 3);
		}

		// 2x the lowest probability of the variety
		SparseIntArray varieties = getVarietyIndexes(currentMap, resources, probs);
		for (int i = 0; i < varieties.size(); i++) {
			addValue(sums, varieties.keyAt(i), 2 * varieties.valueAt(i));
		}

		// Add reasons
		SparseArray<ArrayList<String>> reasons = new SparseArray<>();
		for (int i = 0; i < sums.size(); i++) {
			int key = sums.keyAt(i);
			reasons.put(key, new ArrayList<String>());
			if (highProbs.get(key) != 0) { reasons.get(key).add(Trait.RICH); }
			if (rareResources.get(key) != 0) { reasons.get(key).add(Trait.RARE); }
			if (factories.get(key) != 0) { reasons.get(key).add(Trait.FACTORY); }
			if (traders.get(key) != 0) { reasons.get(key).add(Trait.TRADER); }
			if (roads.get(key) != 0) { reasons.get(key).add(Trait.ROAD_BUILDER); }
			if (cities.get(key) != 0) { reasons.get(key).add(Trait.CITY_BUILDER); }
			if (devCards.get(key) != 0) { reasons.get(key).add(Trait.DEV_CARD_BUILDER); }
			if (ninjas.get(key) != 0) { reasons.get(key).add(Trait.NINJA); }
			if (varieties.get(key) != 0) { reasons.get(key).add(Trait.VARIETY); }
		}

		//BetterLog.v("   RICH    : " + highProbs);
		//BetterLog.v("   RARE    : " + rareResources);
		//BetterLog.v("   FACTORY : " + factories);
		//BetterLog.v("   TRADER  : " + traders);
		//BetterLog.v("   ROAD    : " + roads);
		//BetterLog.v("   CITY    : " + cities);
		//BetterLog.v("   DEV CARD: " + devCards);
		//BetterLog.v("   NINJA   : " + ninjas);
		//BetterLog.v("   VARIETY : " + varieties);

		//BetterLog.v("Map: " + sums);
		int sumSize = sums.size();
		for (int i = 0; i < sumSize; i++) {
			int index = removeMax(sums);
			// Don't count single resource hexes
			if (currentMap.landIntersections[index].length >= 2) {
				order.add(index);
				set.put(index, reasons.get(index));
			}
		}

		//BetterLog.v("List: " + set);

		return Pair.create(order, set);
	}

	/**
	 * Takes in a list of resources and returns which intersections have 3 different
	 * resources (no two of the same resource at an intersection).
	 * 
	 * @param currentMap Size of the map
	 * @param resources The list of ordered resources.
	 * @param probs The list of ordered probabilities
	 * @return A list of indexes mapped to the resource with the least probability
	 */
	private static SparseIntArray getVarietyIndexes(CatanMap currentMap,
			ArrayList<Resource> resources,	ArrayList<Integer> probs) {
		SparseIntArray set = new SparseIntArray();

		for (int i = 0; i < currentMap.landIntersections.length; i++) {
			if (currentMap.placementIndexes[i] == null || currentMap.placementIndexes[i].length == 0) {
				continue;
			}
			
			int[] triplet = currentMap.landIntersections[i];
			// Don't count shoreline
			if (triplet.length < 3) {
				continue;
			}

			Set<Resource> seen = new HashSet<>();
			boolean containsDuplicates = false;
			int lowestProb = Integer.MAX_VALUE;
			for (int trip : triplet) {
				Resource res = resources.get(trip);
				int prob = probs.get(trip);

				// Deserts should not be considered for variety
				if (res == Resource.DESERT) {
					containsDuplicates = true;
					break;
				}

				if (seen.contains(res)) {
					containsDuplicates = true;
					break;
				} else {
					if (prob >= 0 && PROBABILITY_MAPPING[prob] < lowestProb) {
						lowestProb = PROBABILITY_MAPPING[prob];
					}
					seen.add(resources.get(trip));
				}
			}

			if (!containsDuplicates) {
				set.put(i, lowestProb);
			}
		}

		return set;
	}

	private static SparseIntArray getRoadBuilder(CatanMap currentMap, int n,
			ArrayList<Integer> probs, ArrayList<Resource> resources) {
		ArrayList<Resource> goal = new ArrayList<>();
		goal.add(Resource.WOOD);
		goal.add(Resource.CLAY);

		return getGoodCombinationProbabilities(currentMap, n, probs, resources, goal);
	}

	private static SparseIntArray getCityBuilder(CatanMap currentMap, int n,
			ArrayList<Integer> probs, ArrayList<Resource> resources) {
		ArrayList<Resource> goal = new ArrayList<>();
		goal.add(Resource.ROCK);
		goal.add(Resource.WHEAT);

		return getGoodCombinationProbabilities(currentMap, n, probs, resources, goal);
	}

	private static SparseIntArray getDevCardBuilder(CatanMap currentMap, int n,
			ArrayList<Integer> probs, ArrayList<Resource> resources) {
		ArrayList<Resource> goal = new ArrayList<>();
		goal.add(Resource.ROCK);
		goal.add(Resource.WHEAT);
		goal.add(Resource.SHEEP);

		return getGoodCombinationProbabilities(currentMap, n, probs, resources, goal);
	}

	/**
	 * Takes in a list of established probabilities and resources for a map and returns
	 * a list of all intersections that have decent (at least n) combination of the
	 * resources provided.
	 * 
	 * @param currentMap Size of the map
	 * @param n How much of each resource it needs to be returned
	 * @param probs The list of ordered probabilities.
	 * @param resources The list of ordered resources.
	 * @param goal The resources needed for this good combination.
	 * @return A list of indexes to the sum of those resources
	 */
	private static SparseIntArray getGoodCombinationProbabilities(CatanMap currentMap, int n,
			ArrayList<Integer> probs, ArrayList<Resource> resources, ArrayList<Resource> goal) {
		SparseIntArray set = new SparseIntArray();

		for (int i = 0; i < currentMap.landIntersections.length; i++) {
			if (currentMap.placementIndexes[i] == null || currentMap.placementIndexes[i].length == 0) {
				continue;
			}
			
			int totalSum = 0;
			int[] triplet = currentMap.landIntersections[i];

			Map<Resource, Integer> resourceSums = new HashMap<>();
			for (Resource res : goal) {
				resourceSums.put(res, 0);
			}

			for (int trip : triplet) {
				Resource neighborResource = resources.get(trip);
				int neighborProb = probs.get(trip);

				if (resourceSums.containsKey(resources.get(trip)) && neighborProb >= 0) {
					// Skip no prob resources
					resourceSums.put(neighborResource,
							resourceSums.get(neighborResource) + PROBABILITY_MAPPING[neighborProb]);
					totalSum += PROBABILITY_MAPPING[neighborProb];
				}
			}

			boolean hasEnough = true;
			for (Map.Entry<Resource, Integer> resourceSum : resourceSums.entrySet()) {
				if (resourceSum.getValue() < n) {
					hasEnough = false;
					break;
				}
			}

			if (hasEnough) {
				set.put(i, totalSum);
			}
		}

		return set;
	}

	/**
	 * Takes in a list of established probabilities for a map and returns a mapping
	 * of the top probabilities that are at least as big as n when adding up the dots.  Map 
	 * is keyed by their index in the ordered probabilities and value is how many dots.
	 * 
	 * @param currentMap Size of the map
	 * @param n The value of how many dots a probability must have.
	 * @param probs The list of ordered probabilities
	 * @param ninja Option as to whether to ignore intersections with 6/8s.
	 * @return A map of indexes to their sum of dots
	 */
	private static SparseIntArray getHighProbabilities(CatanMap currentMap, int n,
			ArrayList<Integer> probs, boolean ninja) {
		SparseIntArray set = new SparseIntArray();

		for (int i = 0; i < currentMap.landIntersections.length; i++) {
			if (currentMap.placementIndexes[i] == null || currentMap.placementIndexes[i].length == 0) {
				continue;
			}
			
			int[] triplet = currentMap.landIntersections[i];
			if (ninja) {
				boolean validTarget = true;
				for (int trip : triplet) {
					// Ignore 6/8s
					if (probs.get(trip) >= 0 && PROBABILITY_MAPPING[probs.get(trip)] == 5) {
						validTarget = false;
						break;
					}
				}
				if (!validTarget) {
					continue;
				}
			}

			int sum = 0;
			for (int trip : triplet) {
				// Skip no prob resources
				if (probs.get(trip) >= 0) {
					sum += PROBABILITY_MAPPING[probs.get(trip)];
				}
			}

			if (sum >= n) {
				set.put(i, sum);
			}
		}

		return set;
	}

	/**
	 * Takes in a full map and returns a mapping of the top placements that have
	 * good probability of resources and are on a 3:1.
	 * 
	 * @param currentMap Size of the map
	 * @param n The number of probability dots to be good enough
	 * @param probs The list of ordered probabilities
	 * @param resources The list of ordered resources
	 * @param harbors The list of ordered harbors
	 * @return A map of indexes to their number of dots
	 */
	private static SparseIntArray getTraderPlacements(CatanMap currentMap, int n,
			ArrayList<Integer> probs, ArrayList<Resource> resources, ArrayList<Harbor> harbors) {
		SparseIntArray set = new SparseIntArray();

		// Find out how many land only intersections first
		int landOnly = 0;		
		for (int i = 0; i < currentMap.landIntersections.length; i++) {
			if (currentMap.placementIndexes[i] == null || currentMap.placementIndexes[i].length == 0) {
				continue;
			}
			
			int[] landIntersections = currentMap.landIntersections[i];
			if (landIntersections.length == 3) {
				landOnly = i + 1; // 0 indexing
				continue;
			}
			
			// Avoid NPE
			if ((i - landOnly) >= harbors.size()) {
				continue;
			}

			Resource harborResource = harbors.get(i - landOnly).resource;
			if (harborResource != Resource.DESERT) {
				continue; // Only 3:1 harbors
			}
			int landSum = 0;
			int[] landIndexes = currentMap.waterNeighbors[i - landOnly];
			//if (landIndexes.length < 2) {
			//	continue; // We only want harbors with two resources
			//}
			for (int landIndex : landIndexes) {
				// Skip no prob resources
				if (probs.get(landIndex) < 0) {
					continue;
				}
				landSum += PROBABILITY_MAPPING[probs.get(landIndex)];
			}
			if (landSum >= n) {
				set.put(i, landSum);
			}
		}		

		return set;
	}

	/**
	 * Takes in a full map and returns a mapping of the top placements that have
	 * good probability of a resource and are on a 2:1.
	 * 
	 * @param currentMap Size of the map
	 * @param n The number of probability dots to be good enough
	 * @param probs The list of ordered probabilities
	 * @param resources The list of ordered resources
	 * @param harbors The list of ordered harbors
	 * @return A map of indexes to their number of dots
	 */
	private static SparseIntArray getFactoryPlacements(CatanMap currentMap, int n,
			ArrayList<Integer> probs, ArrayList<Resource> resources, ArrayList<Harbor> harbors) {
		SparseIntArray set = new SparseIntArray();

		// Find out how many land only intersections first
		int landOnly = 0;		
		for (int i = 0; i < currentMap.landIntersections.length; i++) {
			if (currentMap.placementIndexes[i] == null || currentMap.placementIndexes[i].length == 0) {
				continue;
			}
			
			int[] landIntersections = currentMap.landIntersections[i];
			if (landIntersections.length == 3) {
				landOnly = i + 1; // 0 indexing
				continue;
			}
			
			// Avoid NPE
			if ((i - landOnly) >= harbors.size()) {
				continue;
			}
			
			Resource harborResource = harbors.get(i - landOnly).resource;
			if (harborResource == Resource.DESERT || harborResource == Resource.WATER) {
				continue; // Only harbors
			}
			int landSum = 0;
			int[] landIndexes = currentMap.waterNeighbors[i - landOnly];
			//if (landIndexes.length < 2) {
			//	continue; // We only want harbors with two resources
			//}
			for (int landIndex : landIndexes) {
				// Skip no prob resources
				if (probs.get(landIndex) < 0) {
					continue;
				}
				Resource landResource = resources.get(landIndex);
				int landProb = PROBABILITY_MAPPING[probs.get(landIndex)];

				if (landResource == harborResource) {
					landSum += landProb;
				}
			}
			if (landSum >= n) {
				set.put(i, landSum);
			}
		}		

		return set;
	}

	/**
	 * Takes in a list of established probabilities/resources for a map and returns a mapping
	 * of the top resources that are at least n% of the full resource on the map.  Map 
	 * is keyed by their index in the ordered probabilities and value is what percentage.
	 * 
	 * @param currentMap Size of the map
	 * @param n The percentage of which the resource must be of the full value
	 * @param probs The list of ordered probabilities
	 * @param resources The list of ordered resources
	 * @return A map of indexes to their percentage
	 */
	private static SparseIntArray getRareResources(CatanMap currentMap, int n,
			ArrayList<Integer> probs, ArrayList<Resource> resources) {
		SparseIntArray set = new SparseIntArray();

		Map<Resource, Integer> sums = new HashMap<>();
		for (int i = 0; i < probs.size(); i++) {
			// Skip no prob resources
			if (probs.get(i) < 0) {
				continue;
			}
			int prob = PROBABILITY_MAPPING[probs.get(i)];
			Resource res = resources.get(i);

			if (sums.get(res) == null) {
				sums.put(res, 0);
			}
			sums.put(res, sums.get(res) + prob);
		}

		for (int i = 0; i < probs.size(); i++) {
			// Skip no prob resources
			if (probs.get(i) < 0) {
				continue;
			}
			int prob = PROBABILITY_MAPPING[probs.get(i)];
			Resource res = resources.get(i);

			if (res == Resource.DESERT) {
				continue;
			}

			int percent = prob * 100 / sums.get(res);
			if (percent >= n) {
				for (int tripletIndex : currentMap.landIntersectionIndexes[i]) {
					if (currentMap.placementIndexes[tripletIndex] != null && currentMap.placementIndexes[tripletIndex].length != 0) {
						set.put(tripletIndex, percent);
					}
				}
			}			
		}

		return set;
	}
} public class MapView extends View {
	private static final int INVALID_POINTER_ID = -1;
	
	private static final Paint RED_PAINT;
	private static final Paint GENERAL_PAINT;
	private static final Paint PROB_DOTS_PAINT;
	private static final Paint REASON_PAINT;
	private static final Path BUBBLE_PATH;
	private static final Paint TEXT_PAINT;
	private static final Paint TEST_PAINT;
	static {
		RED_PAINT = new Paint();
		RED_PAINT.setColor(Color.RED);
		RED_PAINT.setStyle(Style.STROKE);

		GENERAL_PAINT = new Paint();
		GENERAL_PAINT.setTextAlign(Paint.Align.CENTER);
		GENERAL_PAINT.setTypeface(Typeface.DEFAULT_BOLD);
		GENERAL_PAINT.setAntiAlias(true);

		PROB_DOTS_PAINT = new Paint();
		PROB_DOTS_PAINT.setColor(0xFF000000);
		PROB_DOTS_PAINT.setAlpha(200);
		PROB_DOTS_PAINT.setAntiAlias(true);

		REASON_PAINT = new Paint();
		REASON_PAINT.setColor(0xFF000000);
		REASON_PAINT.setAlpha(175);
		REASON_PAINT.setAntiAlias(true);
		
		BUBBLE_PATH = new Path();
		
		TEXT_PAINT = new Paint();
		TEXT_PAINT.setTextAlign(Paint.Align.LEFT);
		TEXT_PAINT.setStrokeWidth(1);
		TEXT_PAINT.setAntiAlias(true);
		TEXT_PAINT.setColor(0xFFFFFFFF);
		
		TEST_PAINT = new Paint();
		TEST_PAINT.setTextAlign(Paint.Align.CENTER);
		TEST_PAINT.setTypeface(Typeface.DEFAULT_BOLD);
		TEST_PAINT.setAntiAlias(true);
	}

	private static final float XR = 17f;
	private static final float XR_2 = 9f;
	private static final float YR1 = 18f;
	private static final float YR2 = 11f;
	private static final float YR2_2 = 6f;
	private static final float HEX_R = 15.59f;
	private static final float BORDER_WIDTH = 2f;
	private static final float PROBABILITY_DOT_RADIUS = 1.5f;
	private static final float PLACEMENT_DOT_RADIUS = 5f;
	private static final float PLACEMENT_TRIANGLE_HEIGHT = 25f;
	private static final float PROBABILITY_TOP_BUFFER = -2f;
	private static final float HARBOR_CIRCLE_RADIUS = 10f;
	
	private float dFontSize;
	private float dReasonFontSize;
	private float dReasonFontBufferMultiplier;
	private float dViewBuffer;
	private float dPlacementReasonBoxWidth;
	private float dPlacementReasonBoxLineHeight;
	
	private int mMaxX;
	private int mMaxY;
	
	private boolean mReady;
	
	private int[][] mPiecesX;
	private int[][] mPiecesY;
	private int[][] mPiecesColor;
	private int[][] mUPiecesX;
	private int[][] mUPiecesY;
	private int[][] mUPiecesColor;

	private ShapeDrawable mHexShapeDrawable;
	private Rect mDrawRect;
	private Rect mDotsRect;
	private RectF mReasonsRectF;
	
	private boolean[][] mUVisibility;
	private int[][] mProbs;
	private int[][] mUProbs;
	private ArrayList<Harbor> mHarbors;
	private MapSize mMapSize;
	private int mPlacementBookmark;
	private SparseArray<ArrayList<String>> mPlacements;
	private ArrayList<Integer> mOrderedPlacements;

	private ScaleGestureDetector mScaleDetector;
	private GestureDetector mTapDetector;
	private float mScaleFactor;
	private float mTextScaleFactor;
	private float mDiffX;
	private float mDiffY;
    private float mLastTouchX;
    private float mLastTouchY;
    // The "active pointer" is the one currently moving our object.
    private int mActivePointerId = INVALID_POINTER_ID;
	
	public MapView(Context context) {
		super(context);

		init(context);
	}

	public MapView(Context context, AttributeSet attrs) {
		super(context, attrs);

		init(context);
	}

	private void init(Context context) {
		BetterLog.d("INIT");
		initDimens(context);
		
		BetterLog.d("mReady=false");
		mReady = false;

		mPiecesX = new int[BOARD_RANGE_X + 1][BOARD_RANGE_Y + 1];
		mPiecesY = new int[BOARD_RANGE_X + 1][BOARD_RANGE_Y + 1];
		mPiecesColor = new int[BOARD_RANGE_X + 1][BOARD_RANGE_Y + 1];
		
		mUPiecesX = new int[BOARD_RANGE_X + 1][BOARD_RANGE_Y + 1];
		mUPiecesY = new int[BOARD_RANGE_X + 1][BOARD_RANGE_Y + 1];
		mUPiecesColor = new int[BOARD_RANGE_X + 1][BOARD_RANGE_Y + 1];

		mUVisibility = new boolean[BOARD_RANGE_X + 1][BOARD_RANGE_Y + 1];
		
		mHexShapeDrawable = new ShapeDrawable(new HexShape());
		mDrawRect = new Rect();
		mDotsRect = new Rect();
		mReasonsRectF = new RectF();
		
		mProbs = new int[BOARD_RANGE_X + 1][BOARD_RANGE_Y + 1];
		mUProbs = new int[BOARD_RANGE_X + 1][BOARD_RANGE_Y + 1];
		mHarbors = new ArrayList<>();
		mPlacementBookmark = -1; // No placements initially
		mPlacements = new SparseArray<>();
		mOrderedPlacements = new ArrayList<>();

		mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
		mTapDetector = new GestureDetector(context, new TapListener());
		
		mTextScaleFactor = 1.0f;
	}
	
	private void initDimens(Context context) {
		Resources res = getResources();

		dFontSize = (float) res.getDimension(R.dimen.font_size);
		dReasonFontSize = (float) res.getDimension(R.dimen.reason_font_size);
		dReasonFontBufferMultiplier = (float) res.getDimension(R.dimen.reason_font_buffer_multiplier);
		dViewBuffer = (float) res.getDimension(R.dimen.view_buffer);
		dPlacementReasonBoxWidth = (float) res.getDimension(R.dimen.placement_reason_box_width);
		dPlacementReasonBoxLineHeight = (float) res.getDimension(R.dimen.placement_reason_box_line_height);
	}
	
	private static class SavedState extends BaseSavedState {
		private boolean ready;
		private int maxX;
		private int maxY;
		private int[][] piecesX;
		private int[][] piecesY;
		private int[][] piecesColor;
		private int[][] uPiecesX;
		private int[][] uPiecesY;
		private int[][] uPiecesColor;
		private float scaleFactor;
		private float textScaleFactor;
		private MapSize mapSize;
		private int[][] probs;
		private int[][] uProbs;
		private boolean[][] uVisibility;
		private ArrayList<Harbor> harbors;
		private int placementBookmark;
		private SparseArray<ArrayList<String>> placements;
		private ArrayList<Integer> orderedPlacements;

        private SavedState(Parcelable superState) {
            super(superState);
        }
        
		@SuppressWarnings("unchecked")
		private SavedState(Parcel in) {
            super(in);
            ready = in.readByte() == 1;
            maxX = in.readInt();
            maxY = in.readInt();
            piecesX = (int[][]) in.readSerializable();
            piecesY = (int[][]) in.readSerializable();
            piecesColor = (int[][]) in.readSerializable();
            uPiecesX = (int[][]) in.readSerializable();
            uPiecesY = (int[][]) in.readSerializable();
            uPiecesColor = (int[][]) in.readSerializable();
            scaleFactor = in.readFloat();
            textScaleFactor = in.readFloat();
            mapSize = in.readParcelable(MapView.class.getClassLoader());
            probs = (int[][]) in.readSerializable();
            uProbs = (int[][]) in.readSerializable();
            uVisibility = (boolean[][]) in.readSerializable();
            if (harbors == null) {
            	harbors = new ArrayList<>();
            }
            in.readTypedList(harbors, Harbor.CREATOR);
            placementBookmark = in.readInt();
            placements = Util.bundleToSparseArrayArrayList(in.readBundle());
            orderedPlacements = (ArrayList<Integer>) in.readSerializable();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeByte((byte) (ready ? 1 : 0));
            out.writeInt(maxX);
            out.writeInt(maxY);
            out.writeSerializable(piecesX);
            out.writeSerializable(piecesY);
            out.writeSerializable(piecesColor);
            out.writeSerializable(uPiecesX);
            out.writeSerializable(uPiecesY);
            out.writeSerializable(uPiecesColor);
            out.writeFloat(scaleFactor);
            out.writeFloat(textScaleFactor);
            out.writeParcelable(mapSize, 0);
            out.writeSerializable(probs);
            out.writeSerializable(uProbs);
            out.writeSerializable(uVisibility);
            out.writeTypedList(harbors);
            out.writeInt(placementBookmark);
            out.writeBundle(Util.sparseArrayArrayListToBundle(placements));
            out.writeSerializable(orderedPlacements);
        }

        @SuppressWarnings("unused")
		public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
	}
	
	@Override
	protected Parcelable onSaveInstanceState () {
		BetterLog.d("MapView.onSaveInstanceState");
		Parcelable savedState = super.onSaveInstanceState();
		
		SavedState ss = new SavedState(savedState);
		ss.ready = mReady;
		ss.maxX = mMaxX;
		ss.maxY = mMaxY;
		ss.piecesX = mPiecesX;
		ss.piecesY = mPiecesY;
		ss.piecesColor = mPiecesColor;
		ss.uPiecesX = mUPiecesX;
		ss.uPiecesY = mUPiecesY;
		ss.uPiecesColor = mUPiecesColor;
		ss.scaleFactor = mScaleFactor;
		ss.textScaleFactor = mTextScaleFactor;
		ss.mapSize = mMapSize;
		ss.probs = mProbs;
		ss.uProbs = mUProbs;
		ss.uVisibility = mUVisibility;
		ss.harbors = mHarbors;
		ss.placementBookmark = mPlacementBookmark;
		ss.placements = mPlacements;
		ss.orderedPlacements = mOrderedPlacements;
		
		return ss;
	}
	
	@Override
	protected void onRestoreInstanceState (Parcelable state) {
		BetterLog.d("MapView.onRestoreInstanceState");
		if (!(state instanceof SavedState)) {
			super.onRestoreInstanceState(state);
			return;
		}

		SavedState ss = (SavedState) state;
		super.onRestoreInstanceState(ss.getSuperState());

		mReady = ss.ready;
		mMaxX = ss.maxX;
		mMaxY = ss.maxY;
		mPiecesX = ss.piecesX;
		mPiecesY = ss.piecesY;
		mPiecesColor = ss.piecesColor;
		mUPiecesX = ss.uPiecesX;
		mUPiecesY = ss.uPiecesY;
		mUPiecesColor = ss.uPiecesColor;
		mScaleFactor = ss.scaleFactor;
		mTextScaleFactor = ss.textScaleFactor;
		mMapSize = ss.mapSize;
		mProbs = ss.probs;
		mUProbs = ss.uProbs;
		mUVisibility = ss.uVisibility;
		mHarbors = ss.harbors;
		mPlacementBookmark = ss.placementBookmark;
		mPlacements = ss.placements;
		mOrderedPlacements = ss.orderedPlacements;
	}
	
	public void setReady() {
		BetterLog.d("ready=true");
		mReady = true;
	}

	public void setMapSize(MapSize currentMap) {
		BetterLog.d("setMapSize: " + currentMap);
		mMapSize = currentMap;
	}

	public void setLandAndWaterResources(Resource[][] land, Harbor[][] water, Resource[][] unknowns) {
		BetterLog.d("setLAWR");
		mMaxX = 0;
		mMaxY = 0;
		
		for (int i = 0; i < BOARD_RANGE_Y; i++) {
			for (int j = (i % 2); j < BOARD_RANGE_X; j += 2) {
				if (land != null && land[j][i] != null) {
					if (j > mMaxX) {
						mMaxX = j;
					}
					if (i > mMaxY) {
						mMaxY = i;
					}
					mPiecesX[j][i] = j;
					mPiecesY[j][i] = i;
					mPiecesColor[j][i] = land[j][i].color;
				} else if (water != null && water[j][i] != null) {
					if (j > mMaxX) {
						mMaxX = j;
					}
					if (i > mMaxY) {
						mMaxY = i;
					}
					mPiecesX[j][i] = j;
					mPiecesY[j][i] = i;
					mPiecesColor[j][i] = 0xFF1959DF;
				} else {
					mPiecesX[j][i] = j;
					mPiecesY[j][i] = i;
					mPiecesColor[j][i] = 0xFFFFFFFF;
					//mPiecesColor[j][i] = 0xFFCCCCCC;
				}

				if (unknowns != null && unknowns[j][i] != null) {
					if (j > mMaxX) {
						mMaxX = j;
					}
					if (i > mMaxY) {
						mMaxY = i;
					}
					mUPiecesX[j][i] = j;
					mUPiecesY[j][i] = i;
					mUPiecesColor[j][i] = unknowns[j][i].color;
				} else {
					mUPiecesX[j][i] = 0;
					mUPiecesY[j][i] = 0;
					mUPiecesColor[j][i] = 0;
				}
			}
		}
		//BetterLog.v(String.format("Max (%s,%s)", mMaxX, mMaxY));
		// Force the re-draw after finding the max X/Y
		invalidate();
	}

	public void setProbabilities(int[][] probs, int[][] unknownProbs) {
		mProbs = probs;
		mUProbs = unknownProbs;
	}
	
	public boolean[][] getUVisibility() {
		return mUVisibility;
	}
	
	public void setUVisibility(boolean[][] visibles) {
		mUVisibility = visibles;
	}

	public void setHarbors(ArrayList<Harbor> harbors) {
		mHarbors = harbors;
	}
	
	public void setPlacementBookmark(int placementBookmark) {
		mPlacementBookmark = placementBookmark;
	}

	public void setPlacements(SparseArray<ArrayList<String>> placements) {
		mPlacements = placements;
	}

	public void setOrderedPlacements(ArrayList<Integer> orderedPlacements) {
		mOrderedPlacements = orderedPlacements;
	}
	
	public float getScale() {
		return mScaleFactor;
	}
	
	public void setScale(float scale) {
		mScaleFactor = scale;
	}
	
	private double dist(float x1, float y1, float x2, float y2) {
		return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
	}
	
	private Pair<Integer, Integer> getTouchedPiece(float x, float y) {
		for (int i = 0; i < mPiecesX.length; i++) {
			for (int j = 0; j < mPiecesX[i].length; j++) {
				if (mPiecesX != null && mPiecesY != null) {
					adjustDrawRect(mPiecesX[i][j], mPiecesY[i][j]);
					
					if (dist(x, y, mDrawRect.exactCenterX(),
							mDrawRect.exactCenterY()) <= HEX_R * mScaleFactor) {
						return new Pair<>(i, j);
					}
				}
			}
		}
		return null;
	}
	
	private Pair<Integer, Integer> getTouchedUnknownPiece(float x, float y) {
		for (int i = 0; i < mUPiecesX.length; i++) {
			for (int j = 0; j < mUPiecesX[i].length; j++) {
				if (mUPiecesX != null && mUPiecesY != null) {
					adjustDrawRect(mUPiecesX[i][j], mUPiecesY[i][j]);
					
					if (dist(x, y, mDrawRect.exactCenterX(),
							mDrawRect.exactCenterY()) <= HEX_R * mScaleFactor) {
						return new Pair<>(i, j);
					}
				}
			}
		}
		return null;
	}

	public CatanMap getCatanMap() {
		return mMapSize.mapProvider.get();
	}

	private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
		@Override
		public boolean onScale(ScaleGestureDetector detector) {
			float newScaleFactor = mScaleFactor * detector.getScaleFactor();
			if (newScaleFactor <= 10.0f && newScaleFactor >= 0.1f) {
				mScaleFactor = newScaleFactor;
				mTextScaleFactor *= detector.getScaleFactor();
				
				invalidate();
			}
			
			/*
			mTextScaleFactor *= detector.getScaleFactor();

			// Don't let the object get too small or too large.
			mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 10.0f));
			mTextScaleFactor = Math.max(0.1f, Math.min(mTextScaleFactor, 10.0f));
			*/

			//BetterLog.v("MiddleX: " + dMiddleX);
			//BetterLog.v("MiddleY: " + dMiddleY);
			//BetterLog.v("Factor: " + mScaleFactor);
			//invalidate();
			return true;
		}
	}
	
	private class TapListener extends GestureDetector.SimpleOnGestureListener {
		@Override
		public boolean onSingleTapUp(MotionEvent ev) {
			//Toast.makeText(getContext(), String.format("(%f,%f)", ev.getX(), ev.getY()), Toast.LENGTH_SHORT).show();
			//Toast.makeText(getContext(), String.format("dYR2: %f", dYR2), Toast.LENGTH_SHORT).show();
			//Toast.makeText(getContext(), String.format("mScaleFactor: %f", mScaleFactor), Toast.LENGTH_SHORT).show();
			
			// Check for touching unknowns
			Pair<Integer, Integer> pair = getTouchedUnknownPiece(ev.getX(), ev.getY());
			if (pair != null) {
				int i = pair.first;
				int j = pair.second;
				
				if (mUVisibility != null) {
					mUVisibility[i][j] = !mUVisibility[i][j];
					
					invalidate();
				}
			}
			pair = getTouchedPiece(ev.getX(), ev.getY());
			if (pair != null) {
				int i = pair.first;
				int j = pair.second;
				
				if (Consts.DEBUG_MAP) {
					int color = mPiecesColor[i][j];
					if (color == 0xFF00FF00) {
						mPiecesColor[i][j] = 0xFF0000FF;	
					} else if (color == 0xFF0000FF) {
						mPiecesColor[i][j] = 0xFFCCCCCC;	
					} else {
						mPiecesColor[i][j] = 0xFF00FF00;
					}
					invalidate();
				}
			}
			return true;
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		// Let the ScaleGestureDetector inspect all events.
		mScaleDetector.onTouchEvent(ev);
		
		// Let the TapDetector also inspect all events.
		mTapDetector.onTouchEvent(ev);
		
		final int action = ev.getAction();
		switch (action & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_DOWN: {
			final float x = ev.getX();
			final float y = ev.getY();

			mLastTouchX = x;
			mLastTouchY = y;
			mActivePointerId = ev.getPointerId(0);
			break;
		}

		case MotionEvent.ACTION_MOVE: {
			final int pointerIndex = ev.findPointerIndex(mActivePointerId);
			final float x = ev.getX(pointerIndex);
			final float y = ev.getY(pointerIndex);

			// Only move if the ScaleGestureDetector isn't processing a gesture.
			if (!mScaleDetector.isInProgress()) {
				final float dx = x - mLastTouchX;
				final float dy = y - mLastTouchY;

				mDiffX += dx;
				mDiffY += dy;
				
				invalidate();
			}

			mLastTouchX = x;
			mLastTouchY = y;

			break;
		}

		case MotionEvent.ACTION_UP: {
			mActivePointerId = INVALID_POINTER_ID;
			break;
		}

		case MotionEvent.ACTION_CANCEL: {
			mActivePointerId = INVALID_POINTER_ID;
			break;
		}

		case MotionEvent.ACTION_POINTER_UP: {
			final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
			final int pointerId = ev.getPointerId(pointerIndex);
			if (pointerId == mActivePointerId) {
				// This was our active pointer going up. Choose a new
				// active pointer and adjust accordingly.
				final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
				mLastTouchX = ev.getX(newPointerIndex);
				mLastTouchY = ev.getY(newPointerIndex);
				mActivePointerId = ev.getPointerId(newPointerIndex);
			}
			break;
		}
		}
		
		return true;
	}
	
	public void onDrawTest(Canvas canvas) {
		TEST_PAINT.setStrokeWidth(2 * mScaleFactor);
		TEST_PAINT.setTextSize(8 * mScaleFactor);
		
		HexShape hexShape = (HexShape) mHexShapeDrawable.getShape();
		hexShape.setPath(XR * mScaleFactor,
				YR1 * mScaleFactor,
				YR2 * mScaleFactor);
		
		for (int i = 0; i < mPiecesX.length; i++) {
			for (int j = 0; j < mPiecesX[i].length; j++) {
				if (mPiecesX != null && mPiecesY != null) {
					adjustDrawRect(mPiecesX[i][j], mPiecesY[i][j]);
					int color = mPiecesColor[i][j];
					
					mHexShapeDrawable.setBounds(mDrawRect);
					if (color == 0xFF0000FF) {
						mHexShapeDrawable.getPaint().setColor(0xFF0000FF);
					} else if (color == 0xFF00FF00) {
						mHexShapeDrawable.getPaint().setColor(0xFF00FF00);
					} else {
						mHexShapeDrawable.getPaint().setColor(0xFFCCCCCC);
					}
					mHexShapeDrawable.draw(canvas);
					
					canvas.drawText(String.format("(%d,%d)", i, j), mDrawRect.centerX(),
							mDrawRect.centerY() - PROBABILITY_TOP_BUFFER * mScaleFactor, TEST_PAINT);
					//canvas.drawText("" + rect.top, rect.centerX(), rect.centerY() - dYR1 / 2f * mScaleFactor, generalPaint);
					//canvas.drawText("" + rect.bottom, rect.centerX(), rect.centerY() + dYR1 / 2f * mScaleFactor, generalPaint);
					//canvas.drawText("" + rect.left, rect.centerX() - dXR_2 * mScaleFactor, rect.centerY(), generalPaint);
					//canvas.drawText("" + rect.right, rect.centerX() + dXR_2 * mScaleFactor, rect.centerY(), generalPaint);
				}
			}
		}
	}

	@Override
	public void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		// Set up initial scale
		
		if (mScaleFactor == 0f && mMaxX != 0f && mMaxY != 0f) {
			// mMaxX gets + 1 because of the overlap of the horizontal counting hexes
			// we need to count the last half-hex hanging off the right edge
			float baseWidth = ((XR + BORDER_WIDTH) * (mMaxX + 1)) + (2 * dViewBuffer);
			float newScaleByWidth = getWidth() / baseWidth;
			float baseHeight = ((YR1 + YR2 + BORDER_WIDTH) * mMaxY) + (2 * dViewBuffer);
			float newScaleByHeight = getHeight() / baseHeight;
			
			//BetterLog.v("getWidth        : " + getWidth());
			//BetterLog.v("baseWidth       : " + baseWidth);
			//BetterLog.v("newScaleByWidth : " + newScaleByWidth);

			//BetterLog.v("getHeight       : " + getHeight());
			//BetterLog.v("baseHeight      : " + baseHeight);
			//BetterLog.v("newScaleByHeight: " + newScaleByHeight);
			if (newScaleByWidth < newScaleByHeight) {
				mScaleFactor = newScaleByWidth;
			} else {
				mScaleFactor = newScaleByHeight;
			}
			//BetterLog.v("MapView.onSizeChanged() new scale: " + mScaleFactor);
		}
		
		//canvas.save();
	    //canvas.translate(mPosX, mPosY);
		
		if (Consts.DEBUG_MAP) {
			onDrawTest(canvas);
			return;
		}
		if (Consts.DEBUG_MAP_WITH_RED) {
			for (int i = 0; i < mPiecesX.length; i++) {
				for (int j = 0; j < mPiecesX[i].length; j++) {
					if (mPiecesX != null && mPiecesY != null) {
						adjustDrawRect(mPiecesX[i][j], mPiecesY[i][j]);
						
						canvas.drawRect(mDrawRect, RED_PAINT);
					}
				}
			}	
		}
		
		if (!mReady) {
			BetterLog.w("tried to draw before ready. not drawing");
			return;
		}
		
		GENERAL_PAINT.setStrokeWidth(2 * mScaleFactor);
		GENERAL_PAINT.setTextSize(dFontSize * mTextScaleFactor);

		HexShape hexShape = (HexShape) mHexShapeDrawable.getShape();
		hexShape.setPath(XR * mScaleFactor,
				YR1 * mScaleFactor,
				YR2 * mScaleFactor);
		
		// First draw the real ones
		for (int i = 0; i < mPiecesX.length; i++) {
			for (int j = 0; j < mPiecesX[i].length; j++) {
				if (mPiecesX != null && mPiecesY != null) {
					adjustDrawRect(mPiecesX[i][j], mPiecesY[i][j]);
					
					mHexShapeDrawable.setBounds(mDrawRect);
					mHexShapeDrawable.getPaint().setColor(mPiecesColor[i][j]);
					mHexShapeDrawable.draw(canvas);
				}
			}
		}
		for (int i = 0; i < mPiecesX.length; i++) {
			for (int j = 0; j < mPiecesX[i].length; j++) {
				if (mPiecesX != null && mPiecesY != null) {
					adjustDrawRect(mPiecesX[i][j], mPiecesY[i][j]);
					
					if (mProbs != null && mProbs[i][j] != 0 && mProbs[i][j] != -1) {
						int prob = mProbs[i][j];
						if (mProbs[i][j] < 0) {
							prob *= -1;
						}
						
						if (prob > 12) {
							// Split into two
							String[] two = Integer.toString(prob).split("7");
							int oneInt = Integer.parseInt(two[0]);
							if (oneInt == 6 || oneInt == 8) {
								GENERAL_PAINT.setColor(0xFFFF0000);
							} else {
								GENERAL_PAINT.setColor(0xFF000000);
							}
							canvas.drawText(Integer.toString(oneInt), mDrawRect.centerX() - XR_2 * mScaleFactor,
									mDrawRect.centerY() + YR2_2 * mScaleFactor - PROBABILITY_TOP_BUFFER * mScaleFactor, GENERAL_PAINT);
							
							mDotsRect.set((int) (mDrawRect.left - XR_2 * mScaleFactor),
									(int) (mDrawRect.top + YR2_2 * mScaleFactor),
									(int) (mDrawRect.right - XR_2 * mScaleFactor),
									(int) (mDrawRect.bottom + YR2_2 * mScaleFactor));
							drawDots(canvas, GENERAL_PAINT, oneInt, mDotsRect);

							int twoInt = Integer.parseInt(two[1]);
							if (twoInt == 6 || twoInt == 8) {
								GENERAL_PAINT.setColor(0xFFFF0000);
							} else {
								GENERAL_PAINT.setColor(0xFF000000);
							}
							canvas.drawText(Integer.toString(twoInt), mDrawRect.centerX() + XR_2 * mScaleFactor,
									mDrawRect.centerY() - YR2_2 * mScaleFactor - PROBABILITY_TOP_BUFFER * mScaleFactor, GENERAL_PAINT);
							
							mDotsRect.set((int) (mDrawRect.left + XR_2 * mScaleFactor),
									(int) (mDrawRect.top - YR2_2 * mScaleFactor),
									(int) (mDrawRect.right + XR_2 * mScaleFactor),
									(int) (mDrawRect.bottom - YR2_2 * mScaleFactor));
							drawDots(canvas, GENERAL_PAINT, twoInt, mDotsRect);
						} else {
							if (prob == 6 || prob == 8) {
								GENERAL_PAINT.setColor(0xFFFF0000);
							} else {
								GENERAL_PAINT.setColor(0xFF000000);
							}
							canvas.drawText(Integer.toString(prob), mDrawRect.centerX(),
									mDrawRect.centerY() - PROBABILITY_TOP_BUFFER * mScaleFactor, GENERAL_PAINT);
							drawDots(canvas, GENERAL_PAINT, prob, mDrawRect);
						}
					}
				}
			}
		}
		
		// Next draw unknowns
		for (int i = 0; i < mUPiecesX.length; i++) {
			for (int j = 0; j < mUPiecesX[i].length; j++) {
				if (mUPiecesX != null && mUPiecesY != null && mUPiecesX[i][j] > 0) {
					adjustDrawRect(mUPiecesX[i][j], mUPiecesY[i][j]);

					mHexShapeDrawable.setBounds(mDrawRect);
					if (mUVisibility != null && !mUVisibility[i][j]) {
						mHexShapeDrawable.getPaint().setColor(0xFFCCCCCC);
					} else {
						mHexShapeDrawable.getPaint().setColor(mUPiecesColor[i][j]);
					}
					mHexShapeDrawable.draw(canvas);

					if (mUVisibility == null || mUVisibility[i][j]) {
						if (mUProbs != null && mUProbs[i][j] > 0) {
							if (mUProbs[i][j] == 6 || mUProbs[i][j] == 8) {
								GENERAL_PAINT.setColor(0xFFFF0000);
							} else {
								GENERAL_PAINT.setColor(0xFF000000);
							}
							canvas.drawText(Integer.toString(mUProbs[i][j]), mDrawRect.centerX(),
									mDrawRect.centerY() - PROBABILITY_TOP_BUFFER * mScaleFactor, GENERAL_PAINT);
							drawDots(canvas, GENERAL_PAINT, mUProbs[i][j], mDrawRect);
						}
					}
				}
			}
		}
		
		CatanMap map = mMapSize.mapProvider.get();
		
		for (int i = 0; i < mHarbors.size(); i++) {
			if (mHarbors.get(i) != null) {
				Point point = getCatanMap().waterGrid[i];
				
				adjustDrawRect(mPiecesX[point.x][point.y], mPiecesY[point.x][point.y]);
				
				drawHarbor(map, canvas, GENERAL_PAINT, mHarbors.get(i), mDrawRect, i);
			}
		}

		if (mPlacementBookmark >= 0) {
			int placementIndex = mOrderedPlacements.get(mPlacementBookmark);

			// Ignore any places that aren't allowed settlements on the coast
			if (map.placementIndexes[placementIndex].length == 2) {
				int landNeighbor = map.placementIndexes[placementIndex][0];
				int landDirection = map.placementIndexes[placementIndex][1];

				Point point = map.landGrid[landNeighbor];
				adjustDrawRect(mPiecesX[point.x][point.y], mPiecesY[point.x][point.y]);
				float x = mDrawRect.centerX() + calculatePlacementOffsetX(landDirection);
				float y = mDrawRect.centerY() + calculatePlacementOffsetY(landDirection);

				// Draw circle
				canvas.drawCircle(x, y, PLACEMENT_DOT_RADIUS * mScaleFactor, PROB_DOTS_PAINT);

				// Draw reasons box
				drawReasonsBox(canvas, mPlacementBookmark, mPlacements.get(placementIndex), x, y);
			}
		}
		
		//canvas.restore();
	}
	
	private void adjustDrawRect(int x, int y) {
		// mMaxX gets + 1 because of the overlap of the horizontal counting hexes
		// we need to count the last half-hex hanging off the right edge
		// not sure why mMaxY needs it too though :P
		float newGridX = (mMaxX / 2f) + 1 - x;
		float newGridY = (mMaxY / 2f) + 1 - y;
		float startX = (getWidth() / 2) - (newGridX * (XR + BORDER_WIDTH)) * mScaleFactor + mDiffX;
		float startY = (getHeight() / 2) - (newGridY * (YR1 + YR2 + BORDER_WIDTH)) * mScaleFactor + mDiffY;
		
		mDrawRect.set((int) startX, (int) startY,
				(int) (startX + (2 * XR * mScaleFactor)),
				(int) (startY + (((2 * YR2) + YR1) * mScaleFactor)));
	}

	private void drawReasonsBox(Canvas canvas, int index, List<String> reasons, float x, float y) {
		float yLevel = y - PLACEMENT_TRIANGLE_HEIGHT;
		mReasonsRectF.set(	x - dPlacementReasonBoxWidth / 3,
				yLevel - (reasons.size() + 1) * dPlacementReasonBoxLineHeight, // +1 since words are bottom aligned
				x + dPlacementReasonBoxWidth * 2/3,
				yLevel);
		canvas.drawRoundRect(mReasonsRectF, 5.0f, 5.0f, REASON_PAINT);

		// Draw bubble connector
		BUBBLE_PATH.reset();
		BUBBLE_PATH.moveTo(x, y);
		BUBBLE_PATH.lineTo(x - XR, yLevel);
		BUBBLE_PATH.lineTo(x, yLevel);
		BUBBLE_PATH.lineTo(x, y);
		canvas.drawPath(BUBBLE_PATH, REASON_PAINT);

		TEXT_PAINT.setTextSize(dReasonFontSize + 3);

		// +1 for zero-indexing
		canvas.drawText("#" + (index + 1),
				x - dPlacementReasonBoxWidth / 3 + XR / 2,
				y - PLACEMENT_TRIANGLE_HEIGHT - reasons.size() * dPlacementReasonBoxLineHeight / 2,
				TEXT_PAINT);

		yLevel -= reasons.size() * dPlacementReasonBoxLineHeight;
		for (String reason : reasons) {
			canvas.drawText(reason, x - dPlacementReasonBoxWidth / 3 + dReasonFontBufferMultiplier * XR, yLevel, TEXT_PAINT);
			yLevel += dPlacementReasonBoxLineHeight;
		}
	}

	private void drawDots(Canvas canvas, Paint paint, int prob, Rect rect) {
		float x = rect.centerX();
		float y = rect.centerY() + YR1 / 3f * mScaleFactor;
		paint.setAntiAlias(false);
		switch (PROBABILITY_MAPPING[prob]) {
		case 5:
			canvas.drawCircle(x - (XR / 2 * mScaleFactor), y, PROBABILITY_DOT_RADIUS * mScaleFactor, paint);
			canvas.drawCircle(x + (XR / 2 * mScaleFactor), y, PROBABILITY_DOT_RADIUS * mScaleFactor, paint);
		case 3:
			canvas.drawCircle(x - (XR / 4 * mScaleFactor), y, PROBABILITY_DOT_RADIUS * mScaleFactor, paint);
			canvas.drawCircle(x + (XR / 4 * mScaleFactor), y, PROBABILITY_DOT_RADIUS * mScaleFactor, paint);
		case 1:
			canvas.drawCircle(x, y, PROBABILITY_DOT_RADIUS * mScaleFactor, paint);
			break;
		case 4:
			canvas.drawCircle(x - (XR * 3/8 * mScaleFactor), y, PROBABILITY_DOT_RADIUS * mScaleFactor, paint);
			canvas.drawCircle(x + (XR * 3/8 * mScaleFactor), y, PROBABILITY_DOT_RADIUS * mScaleFactor, paint);
		case 2:
			canvas.drawCircle(x - (XR * 1/8 * mScaleFactor), y, PROBABILITY_DOT_RADIUS * mScaleFactor, paint);
			canvas.drawCircle(x + (XR * 1/8 * mScaleFactor), y, PROBABILITY_DOT_RADIUS * mScaleFactor, paint);
			break;
		case 0:
		default:
		}
		paint.setAntiAlias(true);
	}

	private void drawHarbor(CatanMap map, Canvas canvas, Paint paint, Harbor harbor, Rect rect, int i) {
		float x = rect.centerX();
		float y = rect.centerY() - (PROBABILITY_TOP_BUFFER + BORDER_WIDTH) * mScaleFactor;
		int dir = MapLogic.whichWayHarborFaces(map, harbor);

		if (harbor.resource == Resource.WATER || dir == -1) {
			return; // Do nothing
		}
		
		if (harbor.resource == Resource.DESERT) {
			paint.setColor(0xFFFFFFFF);
			canvas.drawCircle(x, y, HARBOR_CIRCLE_RADIUS * mScaleFactor, paint);
			drawHarborLine(canvas, paint, map.harborLines[i][dir], x, y);
			drawHarborLine(canvas, paint, map.harborLines[i][dir + 1], x, y);
			paint.setColor(0xFF000000);
			canvas.drawText("3", x, y - 1.5f * PROBABILITY_TOP_BUFFER * mScaleFactor, paint);
		} else {
			paint.setColor(harbor.resource.color);
			canvas.drawCircle(x, y, HARBOR_CIRCLE_RADIUS * mScaleFactor, paint);
			drawHarborLine(canvas, paint, map.harborLines[i][dir], x, y);
			drawHarborLine(canvas, paint, map.harborLines[i][dir + 1], x, y);
			paint.setColor(0xFF000000);
			canvas.drawText("2", x, y - 1.5f * PROBABILITY_TOP_BUFFER * mScaleFactor, paint);
		}
	}

	private void drawHarborLine(Canvas canvas, Paint paint, int dir, float x, float y) {
		switch(dir) {
		case 0:
			canvas.drawLine(x, y, x - XR * mScaleFactor, y - (YR1 / 2) * mScaleFactor, paint);
			break;
		case 1:
			canvas.drawLine(x, y, x, y - (YR2 + (YR1 / 2)) * mScaleFactor, paint);
			break;
		case 2:
			canvas.drawLine(x, y, x + XR * mScaleFactor, y - (YR1 / 2) * mScaleFactor, paint);
			break;
		case 3:
			canvas.drawLine(x, y, x + XR * mScaleFactor, y + (YR1 / 2) * mScaleFactor, paint);
			break;
		case 4:
			canvas.drawLine(x, y, x, y + (YR2 + (YR1 / 2)) * mScaleFactor, paint);
			break;
		case 5:
			canvas.drawLine(x, y, x - XR * mScaleFactor, y + (YR1 / 2) * mScaleFactor, paint);
			break;
		default:
			BetterLog.w("WARNING: Cannot draw this line: "  +  dir);
			break;
		}
	}

	private float calculatePlacementOffsetX(int num) {
		float temp;
		switch (num) {
		case 0:
			temp = -(XR + BORDER_WIDTH);
			break;
		case 1:
			temp = 0;
			break;
		case 2:
			temp = XR + BORDER_WIDTH;
			break;
		case 3:
			temp = XR + BORDER_WIDTH;
			break;
		case 4:
			temp = 0;
			break;
		case 5:
			temp = -(XR + BORDER_WIDTH);
			break;
		default:
			temp = 0;
			break;
		}
		return temp * mScaleFactor;
	}

	private float calculatePlacementOffsetY(int num) {
		float temp;
		switch (num) {
		case 0:
			temp = -YR1/2;
			break;
		case 1:
			temp = -(YR2 + YR1/2);
			break;
		case 2:
			temp = -YR1/2;
			break;
		case 3:
			temp = YR1/2;
			break;
		case 4:
			temp = YR2 + YR1/2;
			break;
		case 5:
			temp = YR1/2;
			break;
		default:
			temp = 0;
			break;
		}
		return (temp - (PROBABILITY_TOP_BUFFER + (2 * BORDER_WIDTH))) * mScaleFactor;
	}
} public final class Analytics {
	private Analytics() {}

	public static final String CATEGORY_MAIN = "main";
	public static final String CATEGORY_MAPS_MENU = "maps_menu";
	public static final String CATEGORY_MENU_MENU = "menu_menu";
	public static final String CATEGORY_SETTLERS_MENU = "seafarers_menu";
	public static final String CATEGORY_SEAFARERS_MENU = "seafarers_menu";
	public static final String CATEGORY_EXPANSION_MENU = "expansion_menu";
	public static final String CATEGORY_ABOUT_MENU = "about_menu";
	public static final String CATEGORY_ROLL_TRACKER = "roll_tracker";

	public static final String ACTION_BUTTON = "button";
	public static final String ACTION_LONG_PRESS_BUTTON = "long_press_button";
	public static final String ACTION_PURCHASE_OFFER = "purchase_offer";
	
	public static final String INFO = "info";
	public static final String RATE_US = "rate_us";

	public static final String SETTINGS = "settings";
	public static final String USE_PLACEMENTS = "placements";
	public static final String NEXT_PLACEMENTS = "next";
	public static final String PREV_PLACEMENTS = "prev";
	public static final String SHUFFLE_MAP = "shuffle_map";
	
	public static final String SEE_SETTLERS = "settlers";
	public static final String SEE_SEAFARERS = "seafarers";
	public static final String SEE_MORE = "more";

	public static final String USE_ROLL_TRACKER = "roll_tracker";
	public static final String SHUFFLE_PROBABILITIES = "shuffle_probabilities";
	public static final String SHUFFLE_HARBORS = "shuffle_harbors";

	public static final String EXPANSION_SMALL = "expansion_small";
	public static final String EXPANSION_LARGE = "expansion_large";

	public static final String DELETE = "delete";

	// Arg 1: MapSize
	// Arg 2: MapType
	public static final String VIEW_MAP_FORMAT = "/map/%s/%s";
	public static final String VIEW_ROLL_TRACKER = "/roll_tracker";
	public static final String VIEW_INFO = "/info";
	public static final String VIEW_MAPS_MENU = "/main_menu";
	public static final String VIEW_EXPANSIONS_MENU = "/expansions_menu";
	public static final String VIEW_MORE_MENU = "/more_menu";
	public static final String VIEW_SETTLERS_MENU = "/settlers_menu";
	public static final String VIEW_SEAFARERS_MENU = "/seafarers_menu";

    private static final Object sLock = new Object();
    private static volatile Tracker sTracker = null;

    public static void track(Context context, String category, String action) {
        getTracker(context).send(getEvent(category, action, null /* action */, 0L /* value */));
    }

    public static void track(Context context, String category, String action, String label) {
        getTracker(context).send(getEvent(category, action, label, 0L /* value */));
    }

    public static void track(Context context, String category, String action, String label,
                             long value) {
        getTracker(context).send(getEvent(category, action, label, value));
    }

    public static void trackView(Context context, String view) {
        Tracker tracker = getTracker(context);
        tracker.setScreenName(view);
        tracker.send(new HitBuilders.AppViewBuilder().build());
    }

    private static Map<String, String> getEvent(String category, String action, String label,
                                                long value) {
        HitBuilders.EventBuilder builder = new HitBuilders.EventBuilder()
                .setCategory(category)
                .setAction(action);
        if (label != null) {
            builder.setLabel(label);
        }
        if (value != 0L) {
            builder.setValue(value);
        }
        return builder.build();
    }

    private static Tracker getTracker(Context context) {
        if (sTracker == null) {
            synchronized (sLock) {
                if (sTracker == null) {
                    sTracker = GoogleAnalytics.getInstance(context.getApplicationContext())
                            .newTracker(R.xml.global_tracker);
                }
            }
        }
        return sTracker;
    }
} public final class BetterLog {
	private static final String TAG = "BetterSettlers";
	
	private BetterLog() {}
	
	/* VERBOSE */
	public static void v(String fmt, Object... args) {
		v(String.format(fmt, args));
	}
	public static void v(Throwable t, String fmt, Object... args) {
		v(t, String.format(fmt, args));
	}
	public static void v(String msg) {
		if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, msg);
	}
	public static void v(Throwable t, String msg) {
		if (Log.isLoggable(TAG, Log.VERBOSE)) Log.v(TAG, msg, t);
	}
	
	/* DEBUG */
	public static void d(String fmt, Object... args) {
		d(String.format(fmt, args));
	}
	public static void d(Throwable t, String fmt, Object... args) {
		d(t, String.format(fmt, args));
	}
	public static void d(String msg) {
		if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, msg);
	}
	public static void d(Throwable t, String msg) {
		if (Log.isLoggable(TAG, Log.DEBUG)) Log.d(TAG, msg, t);
	}
	
	/* INFO */
	public static void i(String fmt, Object... args) {
		i(String.format(fmt, args));
	}
	public static void i(Throwable t, String fmt, Object... args) {
		i(t, String.format(fmt, args));
	}
	public static void i(String msg) {
		Log.i(TAG, msg);
	}
	public static void i(Throwable t, String msg) {
		Log.i(TAG, msg, t);
	}
	
	/* WARNING */
	public static void w(String fmt, Object... args) {
		w(String.format(fmt, args));
	}
	public static void w(Throwable t, String fmt, Object... args) {
		w(t, String.format(fmt, args));
	}
	public static void w(String msg) {
		Log.w(TAG, msg);
	}
	public static void w(Throwable t, String msg) {
		Log.w(TAG, msg, t);
	}
	
	/* ERROR */
	public static void e(String fmt, Object... args) {
		e(String.format(fmt, args));
	}
	public static void e(Throwable t, String fmt, Object... args) {
		e(t, String.format(fmt, args));
	}
	public static void e(String msg) {
		Log.e(TAG, msg);
	}
	public static void e(Throwable t, String msg) {
		Log.e(TAG, msg, t);
	}
} public final class Consts {
	private Consts() {}

	///////////////////////////////////////////
	// All of these should be false for release
	public static final boolean TEST_STATIC_IAB = false;
	public static final boolean TEST_CONSUME_ALL_PURCHASES = false;
	
	public static final boolean DEBUG_MAP = false;
	public static final boolean DEBUG_MAP_WITH_RED = false;
    //
	///////////////////////////////////////////
	
	public static final String LAUNCH_MAP_ACTION = "com.nut.bettersettlers.action.LAUNCH_MAP";
	public static final String PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=com.nut.bettersettlers";

	public static final String SHARED_PREFS_NAME = "Preferences";
	public static final String SHARED_PREFS_KEY_WHATS_NEW_HELP = "ShownWhatsNewVersion21";
	public static final String SHARED_PREFS_KEY_FOG_ISLAND_HELP = "TheFogIsland";
	
	/**
	 * Mapping between the numbers that are shown (rolled on the dice) and the probability of each
	 * being rolled.
	 */
	public static final int[] PROBABILITY_MAPPING = {
		0,  // 0
        0,  // 1
        1,  // 2
        2,  // 3
        3,  // 4
        4,  // 5
        5,  // 6
        0,  // 7
        5,  // 8
        4,  // 9
        3,  // 10
        2,  // 11
        1   // 12
	};
	
	public static final int BOARD_RANGE_X = 22;//19;
	public static final int BOARD_RANGE_HALF_X = 11;//9;
	public static final int BOARD_RANGE_Y = 13;//11;
	public static final int BOARD_RANGE_HALF_Y = 7;//6;
} public final class Util {
	public static boolean equal(Object a, Object b) {
		return a == b || (a != null && a.equals(b));
	}
	
	public static int hashCode(Object... objects) {
		return Arrays.hashCode(objects);
	}
	
	public static Bundle sparseArrayArrayListToBundle(SparseArray<ArrayList<String>> array) {
		Bundle bundle = new Bundle();
		for (int i = 0; i < array.size(); i++) {
			bundle.putStringArrayList(Integer.toString(array.keyAt(i)), array.valueAt(i));
		}
		return bundle;
	}
	
	public static SparseArray<ArrayList<String>> bundleToSparseArrayArrayList(Bundle bundle) {
		SparseArray<ArrayList<String>> array = new SparseArray<>(bundle.size());
		for (String key : bundle.keySet()) {
			array.put(Integer.parseInt(key), bundle.getStringArrayList(key));
		}
		return array;
	}
} public static final int BOARD_RANGE_X = 22;//19; public static final int BOARD_RANGE_Y = 13;//11;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Point;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.util.Pair;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.LinearLayout;
import com.nut.bettersettlers.R;
import com.nut.bettersettlers.activity.MainActivity;
import com.nut.bettersettlers.data.CatanMap;
import com.nut.bettersettlers.data.Harbor;
import com.nut.bettersettlers.data.MapSize;
import com.nut.bettersettlers.data.MapType;
import com.nut.bettersettlers.data.Resource;
import com.nut.bettersettlers.fragment.dialog.MapsDialogFragment;
import com.nut.bettersettlers.fragment.dialog.WelcomeDialogFragment;
import com.nut.bettersettlers.logic.MapLogic;
import com.nut.bettersettlers.logic.PlacementLogic;
import com.nut.bettersettlers.ui.MapView;
import com.nut.bettersettlers.util.Analytics;
import com.nut.bettersettlers.util.BetterLog;
import com.nut.bettersettlers.util.Consts;
import com.nut.bettersettlers.util.Util;
import java.util.ArrayList;
import static com.nut.bettersettlers.util.Consts.BOARD_RANGE_X;
import static com.nut.bettersettlers.util.Consts.BOARD_RANGE_Y;
package com.nut.bettersettlers.fragment;





public class MapFragment extends Fragment {
	private static final String STATE_MAP_SIZE = "MAP_SIZE";
	private static final String STATE_MAP_TYPE = "MAP_TYPE";
	private static final String STATE_RESOURCES = "MAP_RESOURCES";
	private static final String STATE_PROBABILITIES = "PROBABILITIES";
	private static final String STATE_UNKNOWNS = "UNKNOWNS";
	private static final String STATE_UNKNOWN_PROBABILITIES = "UNKNOWN_PROBABILITIES";
	private static final String STATE_HARBORS = "HARBORS";
	private static final String STATE_PLACEMENT_BOOKMARK = "PLACEMENT_BOOKMARK";
	private static final String STATE_PLACEMENT_MAX = "PLACEMENT_MAX";
	private static final String STATE_PLACEMENTS = "PLACEMENTS";
	private static final String STATE_ORDERED_PLACEMENTS = "ORDERED_PLACEMENTS";
	
	private MapView mMapView;

	private ImageView mPlacementsButton;
	private LinearLayout mPlacementsContainer;
	private ImageView mRefreshButton;
	private ImageView mRefreshDownButton;

	private MapSize mMapSize;
	private int mMapType;
	private ArrayList<Harbor> mHarborList = new ArrayList<>();
	private ArrayList<Resource> mResourceList = new ArrayList<>();
	private ArrayList<Integer> mProbabilityList = new ArrayList<>();
	private ArrayList<Resource> mUnknownsList = new ArrayList<>();
	private ArrayList<Integer> mUnknownProbabilitiesList = new ArrayList<>();
	private ArrayList<Integer> mPlacementsList = new ArrayList<>();
	private SparseArray<ArrayList<String>> mOrderedPlacementsList = new SparseArray<>();
	private int mPlacementBookmark = -1;
	private int mPlacementMax;
	
	private boolean mShowSeafarers = false;

	///////////////////////////////
	// Fragment method overrides //
	///////////////////////////////
	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
		super.onCreateView(inflater, container, savedInstanceState);
		View view = inflater.inflate(R.layout.map, container, false);
		mMapView = (MapView) view.findViewById(R.id.map_view);
		
		ImageView settingsButton = (ImageView) view.findViewById(R.id.settings_button);
		settingsButton.setOnClickListener(new View.OnClickListener() {
			@Override public void onClick(View v) {
				FragmentTransaction ft = getActivity().getSupportFragmentManager().beginTransaction();
			    ft.addToBackStack(null);
		    	MapsDialogFragment.newInstance().show(ft, "MapsDialogFragment");
                Analytics.track(getActivity(), Analytics.CATEGORY_MAIN, Analytics.ACTION_BUTTON,
                        Analytics.SETTINGS);
			}
		});
		
		mPlacementsButton = (ImageView) view.findViewById(R.id.placements_button);
		mPlacementsButton.setOnClickListener(new View.OnClickListener() {
			@Override public void onClick(View v) {
		    	if (showingPlacements()) {
		    		hidePlacements(true);
		    	} else {
		    		showPlacements(true);
                    Analytics.track(getActivity(), Analytics.CATEGORY_MAIN, Analytics.ACTION_BUTTON,
                            Analytics.USE_PLACEMENTS);
		    	}
			}
		});
		
		mPlacementsContainer = (LinearLayout) view.findViewById(R.id.placements_container);
		
		ImageView placementsLeftButton = (ImageView) view.findViewById(R.id.placements_left_button);
		placementsLeftButton.setOnClickListener(new View.OnClickListener() {
			@Override public void onClick(View v) {
				prevPlacement();
                Analytics.track(getActivity(), Analytics.CATEGORY_MAIN, Analytics.ACTION_BUTTON,
                        Analytics.PREV_PLACEMENTS);
			}
		});
		
		ImageView placementsRightButton = (ImageView) view.findViewById(R.id.placements_right_button);
		placementsRightButton.setOnClickListener(new View.OnClickListener() {
			@Override public void onClick(View v) {
				nextPlacement();
                Analytics.track(getActivity(), Analytics.CATEGORY_MAIN, Analytics.ACTION_BUTTON,
                        Analytics.NEXT_PLACEMENTS);
			}
		});
		
		mRefreshButton = (ImageView) view.findViewById(R.id.refresh_button);
		mRefreshButton.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View v) {
				asyncMapShuffle();
                Analytics.track(getActivity(), Analytics.CATEGORY_MAIN, Analytics.ACTION_BUTTON,
                        Analytics.SHUFFLE_MAP);
			}
		});
		mRefreshDownButton = (ImageView) view.findViewById(R.id.refresh_down_button);

		// Set initial state of placement buttons
		if (showingPlacements()) {
			showPlacements(false);
		} else {
			hidePlacements(false);
		}
		
		return view;
	}

	/** Called when the activity is going to disappear. */
	@Override
	public void onSaveInstanceState(Bundle outState) {
		super.onSaveInstanceState(outState);

		outState.putParcelable(STATE_MAP_SIZE, mMapSize);
		outState.putInt(STATE_MAP_TYPE, mMapType);
		outState.putParcelableArrayList(STATE_RESOURCES, mResourceList);
		outState.putParcelableArrayList(STATE_UNKNOWNS, mUnknownsList);
		outState.putIntegerArrayList(STATE_PROBABILITIES, mProbabilityList);
		outState.putIntegerArrayList(STATE_UNKNOWN_PROBABILITIES, mUnknownProbabilitiesList);
		outState.putIntegerArrayList(STATE_PLACEMENTS, mPlacementsList);
		outState.putBundle(STATE_ORDERED_PLACEMENTS, Util.sparseArrayArrayListToBundle(mOrderedPlacementsList));
		outState.putParcelableArrayList(STATE_HARBORS, mHarborList);
		outState.putInt(STATE_PLACEMENT_BOOKMARK, mPlacementBookmark);
		outState.putInt(STATE_PLACEMENT_MAX, mPlacementMax);
	}
	
	@Override
	public void onActivityCreated(Bundle savedInstanceState) {
		super.onActivityCreated(savedInstanceState);
		BetterLog.d("MapFragment.onActivityCreated() " + savedInstanceState);
		
		// If savedInstanceState == null, this is the first time the activity is created (not a rotation)
		if (savedInstanceState == null) {
			// Show what's new if we need to
			SharedPreferences prefs = getActivity().getSharedPreferences(Consts.SHARED_PREFS_NAME, Context.MODE_PRIVATE);