001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028
029package edu.wisc.ssec.mcidasv.startupmanager.options;
030
031import java.io.BufferedReader;
032import java.io.BufferedWriter;
033import java.io.File;
034import java.io.FileReader;
035import java.io.FileWriter;
036import java.io.IOException;
037import java.util.ArrayList;
038import java.util.Collection;
039import java.util.Collections;
040import java.util.HashMap;
041import java.util.List;
042import java.util.Map;
043import java.util.Set;
044import java.util.stream.Collectors;
045
046import edu.wisc.ssec.mcidasv.startupmanager.StartupManager;
047import edu.wisc.ssec.mcidasv.startupmanager.Platform;
048
049public class OptionMaster {
050
051    public final static String SET_PREFIX = "SET ";
052    public final static String EMPTY_STRING = "";
053    public final static String QUOTE_STRING = "\"";
054    public final static char QUOTE_CHAR = '"';
055
056    // TODO(jon): write CollectionHelpers.zip() and CollectionHelpers.zipWith()
057    public final Object[][] blahblah = {
058        { "HEAP_SIZE", "Memory", "512m", Type.MEMORY, OptionPlatform.ALL, Visibility.VISIBLE },
059        { "JOGL_TOGL", "Enable JOGL", "1", Type.BOOLEAN, OptionPlatform.UNIXLIKE, Visibility.VISIBLE },
060        { "USE_3DSTUFF", "Enable 3D controls", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
061        { "DEFAULT_LAYOUT", "Load default layout", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
062        { "STARTUP_BUNDLE", "Defaults", "0;", Type.FILE, OptionPlatform.ALL, Visibility.VISIBLE },
063        // mcidasv enables this (the actual property is "visad.java3d.geometryByRef")
064        // by default in mcidasv.properties.
065        { "USE_GEOBYREF", "Enable access to geometry by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
066        { "USE_IMAGEBYREF", "Enable access to image data by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
067        { "USE_NPOT", "Enable Non-Power of Two (NPOT) textures", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
068        // temp bandaid for people suffering from permgen problems.
069        { "USE_CMSGC", "Enable concurrent mark-sweep garbage collector", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
070        { "LOG_LEVEL", "Log Level", "INFO", Type.LOGLEVEL, OptionPlatform.ALL, Visibility.VISIBLE },
071        { "JVM_OPTIONS", "Java Virtual Machine Options", "", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
072        { "TEXTURE_WIDTH", "Texture Size", "4096", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
073    };
074    
075    /**
076     * {@link Option}s can be either platform-specific or applicable to all
077     * platforms. Options that are platform-specific still appear in the 
078     * UI, but their component is not enabled.
079     */
080    public enum OptionPlatform { ALL, UNIXLIKE, WINDOWS };
081    
082    /**
083     * The different types of {@link Option}s.
084     * 
085     * @see TextOption
086     * @see BooleanOption
087     * @see MemoryOption
088     * @see DirectoryOption
089     * @see SliderOption
090     * @see LoggerLevelOption
091     * @see FileOption
092     */
093    public enum Type { TEXT, BOOLEAN, MEMORY, DIRTREE, SLIDER, LOGLEVEL, FILE };
094    
095    /** 
096     * Different ways that an {@link Option} might be displayed.
097     */
098    public enum Visibility { VISIBLE, HIDDEN };
099    
100    /** Maps an option ID to the corresponding object. */
101    private Map<String, ? extends Option> optionMap;
102    
103    private static OptionMaster instance;
104    
105    public OptionMaster() {
106        normalizeUserDirectory();
107        optionMap = buildOptions(blahblah);
108//        readStartup();
109    }
110    
111    public static OptionMaster getInstance() {
112        if (instance == null) {
113            instance = new OptionMaster();
114        }
115        return instance;
116    }
117    
118    /**
119     * Creates the specified options and returns a mapping of the option ID
120     * to the actual {@link Option} object.
121     * 
122     * @param options An array specifying the {@code Option}s to be built.
123     * 
124     * @return Mapping of ID to {@code Option}.
125     * 
126     * @throws AssertionError if the option array contained an entry that
127     * this method cannot build.
128     */
129    private Map<String, Option> buildOptions(final Object[][] options) {
130        // TODO(jon): seriously, get that zip stuff working! this array 
131        // stuff is BAD.
132        Map<String, Option> optMap = new HashMap<>(options.length);
133        
134        for (Object[] arrayOption : options) {
135            String id = (String)arrayOption[0];
136            String label = (String)arrayOption[1];
137            String defaultValue = (String)arrayOption[2];
138            Type type = (Type)arrayOption[3];
139            OptionPlatform platform = (OptionPlatform)arrayOption[4];
140            Visibility visibility = (Visibility)arrayOption[5];
141            
142            switch (type) {
143                case TEXT:
144                    optMap.put(id, new TextOption(id, label, defaultValue, platform, visibility));
145                    break;
146                case BOOLEAN:
147                    optMap.put(id, new BooleanOption(id, label, defaultValue, platform, visibility));
148                    break;
149                case MEMORY:
150                    optMap.put(id, new MemoryOption(id, label, defaultValue, platform, visibility));
151                    break;
152                case DIRTREE:
153                    optMap.put(id, new DirectoryOption(id, label, defaultValue, platform, visibility));
154                    break;
155                case SLIDER:
156                    optMap.put(id, new SliderOption(id, label, defaultValue, platform, visibility));
157                    break;
158                case LOGLEVEL:
159                    optMap.put(id, new LoggerLevelOption(id, label, defaultValue, platform, visibility));
160                    break;
161                case FILE:
162                    optMap.put(id, new FileOption(id, label, defaultValue, platform, visibility));
163                    break;
164                default:
165                     throw new AssertionError(type + 
166                         " is not known to OptionMaster.buildOptions()");
167            }
168        }
169        return optMap;
170    }
171    
172    /**
173     * Converts a {@link Platform} to its corresponding 
174     * {@link OptionPlatform} type.
175     * 
176     * @return The current platform as a {@code OptionPlatform} type.
177     * 
178     * @throws AssertionError if {@link StartupManager#getPlatform()} 
179     * returned something that this method cannot convert.
180     */
181    // a lame-o hack :(
182    protected OptionPlatform convertToOptionPlatform() {
183        Platform platform = StartupManager.getInstance().getPlatform();
184        switch (platform) {
185            case WINDOWS: 
186                return OptionPlatform.WINDOWS;
187            case UNIXLIKE: 
188                return OptionPlatform.UNIXLIKE;
189            default: 
190                throw new AssertionError("Unknown platform: " + platform);
191        }
192    }
193    
194    /**
195     * Returns the {@link Option} mapped to {@code id}.
196     * 
197     * @param id The ID whose associated {@code Option} is to be returned.
198     * 
199     * @return Either the {@code Option} associated with {@code id}, or 
200     * {@code null} if there was no association.
201     * 
202     * 
203     * @see #getMemoryOption
204     * @see #getBooleanOption
205     * @see #getDirectoryOption
206     * @see #getSliderOption
207     * @see #getTextOption
208     * @see #getLoggerLevelOption
209     * @see #getFileOption
210     */
211    private Option getOption(final String id) {
212        return optionMap.get(id);
213    }
214    
215    /**
216     * Searches {@link #optionMap} for the {@link MemoryOption} that 
217     * corresponds with the given {@code id}.
218     * 
219     * @param id Identifier for the desired {@code MemoryOption}. 
220     * Should not be {@code null}.
221     * 
222     * @return Either the {@code MemoryOption} that corresponds to {@code id} 
223     * or {@code null}.
224     */
225    public MemoryOption getMemoryOption(final String id) {
226        return (MemoryOption)optionMap.get(id);
227    }
228    
229    /**
230     * Searches {@link #optionMap} for the {@link BooleanOption} that 
231     * corresponds with the given {@code id}.
232     * 
233     * @param id Identifier for the desired {@code BooleanOption}. 
234     * Should not be {@code null}.
235     * 
236     * @return Either the {@code BooleanOption} that corresponds to {@code id} 
237     * or {@code null}.
238     */
239    public BooleanOption getBooleanOption(final String id) {
240        return (BooleanOption)optionMap.get(id);
241    }
242    
243    /**
244     * Searches {@link #optionMap} for the {@link DirectoryOption} that 
245     * corresponds with the given {@code id}.
246     * 
247     * @param id Identifier for the desired {@code DirectoryOption}. 
248     * Should not be {@code null}.
249     * 
250     * @return Either the {@code DirectoryOption} that corresponds to 
251     * {@code id} or {@code null}.
252     */
253    public DirectoryOption getDirectoryOption(final String id) {
254        return (DirectoryOption)optionMap.get(id);
255    }
256    
257    /**
258     * Searches {@link #optionMap} for the {@link SliderOption} that 
259     * corresponds with the given {@code id}.
260     * 
261     * @param id Identifier for the desired {@code SliderOption}. 
262     * Should not be {@code null}.
263     * 
264     * @return Either the {@code SliderOption} that corresponds to {@code id} 
265     * or {@code null}.
266     */
267    public SliderOption getSliderOption(final String id) {
268        return (SliderOption)optionMap.get(id);
269    }
270    
271    /**
272     * Searches {@link #optionMap} for the {@link TextOption} that 
273     * corresponds with the given {@code id}.
274     * 
275     * @param id Identifier for the desired {@code TextOption}. 
276     * Should not be {@code null}.
277     * 
278     * @return Either the {@code TextOption} that corresponds to {@code id} 
279     * or {@code null}.
280     */
281    public TextOption getTextOption(final String id) {
282        return (TextOption)optionMap.get(id);
283    }
284    
285    /**
286     * Searches {@link #optionMap} for the {@link LoggerLevelOption} that 
287     * corresponds with the given {@code id}.
288     * 
289     * @param id Identifier for the desired {@code LoggerLevelOption}. 
290     * Should not be {@code null}.
291     * 
292     * @return Either the {@code LoggerLevelOption} that corresponds to {@code id} 
293     * or {@code null}.
294     */
295    public LoggerLevelOption getLoggerLevelOption(final String id) {
296        return (LoggerLevelOption)optionMap.get(id);
297    }
298
299    public FileOption getFileOption(final String id) {
300        return (FileOption)optionMap.get(id);
301    }
302
303    // TODO(jon): getAllOptions and optionsBy* really need some work.
304    // I want to eventually do something like:
305    // Collection<Option> = getOpts().byPlatform(WINDOWS, ALL).byType(BOOLEAN).byVis(HIDDEN)
306    /**
307     * Returns all the available startup manager options.
308     * 
309     * @return Either all available startup manager options or an empty 
310     * {@link Collection}.
311     */
312    public Collection<Option> getAllOptions() {
313        return Collections.unmodifiableCollection(optionMap.values());
314    }
315    
316    /**
317     * Returns the {@link Option Options} applicable to the given 
318     * {@link OptionPlatform OptionPlatforms}.
319     * 
320     * @param platforms Desired platforms. Cannot be {@code null}.
321     * 
322     * @return Either a {@link List} of {code Option}-s applicable to
323     * {@code platforms} or an empty {@code List}.
324     */
325    public List<Option> optionsByPlatform(
326        final Collection<OptionPlatform> platforms) 
327    {
328        if (platforms == null) {
329            throw new NullPointerException("must specify platforms");
330        }
331        Collection<Option> allOptions = getAllOptions();
332        List<Option> filteredOptions = 
333            new ArrayList<>(allOptions.size());
334
335        filteredOptions.addAll(
336            allOptions.stream()
337                      .filter(option ->
338                              platforms.contains(option.getOptionPlatform()))
339                      .collect(Collectors.toList()));
340
341        return filteredOptions;
342    }
343    
344    /**
345     * Returns the {@link Option Options} that match the given 
346     * {@link Type Types}. 
347     * 
348     * @param types Desired {@code Option} types. Cannot be {@code null}.
349     * 
350     * @return Either the {@code List} of {@code Option}-s that match the given 
351     * types or an empty {@code List}.
352     */
353    public List<Option> optionsByType(final Collection<Type> types) {
354        if (types == null) {
355            throw new NullPointerException("must specify types");
356        }
357        Collection<Option> allOptions = getAllOptions();
358        List<Option> filteredOptions = 
359            new ArrayList<>(allOptions.size());
360        filteredOptions.addAll(
361            allOptions.stream()
362                      .filter(option -> types.contains(option.getOptionType()))
363                      .collect(Collectors.toList()));
364        return filteredOptions;
365    }
366    
367    /**
368     * Returns the {@link Option Options} that match the given levels of 
369     * {@link Visibility visibility}.
370     * 
371     * @param visibilities Desired visibility levels. Cannot be {@code null}.
372     * 
373     * @return Either the {@code List} of {@code Option}-s that match the given 
374     * visibility levels or an empty {@code List}. 
375     */
376    public List<Option> optionsByVisibility(
377        final Collection<Visibility> visibilities) 
378    {
379        if (visibilities == null) {
380            throw new NullPointerException("must specify visibilities");
381        }
382        Collection<Option> allOptions = getAllOptions();
383        List<Option> filteredOptions = 
384            new ArrayList<>(allOptions.size());
385        filteredOptions.addAll(
386            allOptions.stream()
387                      .filter(option ->
388                              visibilities.contains(option.getOptionVisibility()))
389                      .collect(Collectors.toList()));
390        return filteredOptions;
391    }
392    
393    private void normalizeUserDirectory() {
394        StartupManager startup = StartupManager.getInstance();
395        Platform platform = startup.getPlatform();
396        File dir = new File(platform.getUserDirectory());
397        File prefs = new File(platform.getUserPrefs());
398        
399        if (!dir.exists()) {
400            dir.mkdir();
401        }
402        if (!prefs.exists()) {
403            try {
404                File defaultPrefs = new File(platform.getDefaultPrefs());
405                startup.copy(defaultPrefs, prefs);
406            } catch (IOException e) {
407                System.err.println("Non-fatal error copying user preference template: "+e.getMessage());
408            }
409        }
410    }
411
412    public void readStartup() {
413        String line;
414        
415        File script = 
416            new File(StartupManager.getInstance().getPlatform().getUserPrefs());
417        System.err.println("reading "+script);
418        if (script.getPath().isEmpty()) {
419            return;
420        }
421        try {
422            BufferedReader br = new BufferedReader(new FileReader(script));
423            while ((line = br.readLine()) != null) {
424                if (line.startsWith("#")) {
425                    continue;
426                }
427
428                int splitAt = line.indexOf('=');
429                if (splitAt >= 0) {
430                    String id = line.substring(0, splitAt).replace(SET_PREFIX, EMPTY_STRING);
431                    Option option = getOption(id);
432                    if (option != null) {
433                        System.err.println("setting '"+id+"' with '"+line+'\'');
434                        option.fromPrefsFormat(line);
435                    } else {
436                        System.err.println("Warning: Unknown ID '"+id+'\'');
437                    }
438                } else {
439                    System.err.println("Warning: Bad line format '"+line+'\'');
440                }
441            }
442            br.close();
443        } catch (IOException e) {
444            System.err.println("Non-fatal error reading the user preferences: "+e.getMessage());
445        }
446    }
447    
448    public void writeStartup() {
449        File script = 
450            new File(StartupManager.getInstance().getPlatform().getUserPrefs());
451        if (script.getPath().isEmpty()) {
452            return;
453        }
454        // TODO(jon): use filters when you've made 'em less stupid
455        String newLine = 
456                StartupManager.getInstance().getPlatform().getNewLine();
457        OptionPlatform currentPlatform = convertToOptionPlatform();
458        StringBuilder contents = new StringBuilder(2048);
459        for (Object[] arrayOption : blahblah) {
460            Option option = getOption((String)arrayOption[0]);
461            OptionPlatform platform = option.getOptionPlatform();
462            if ((platform == OptionPlatform.ALL) || (platform == currentPlatform)) {
463                contents.append(option.toPrefsFormat()).append(newLine);
464            }
465        }
466        
467        try {
468            BufferedWriter out = 
469                new BufferedWriter(new FileWriter(script));
470            out.write(contents.toString());
471            out.close();
472        } catch (IOException e) {
473            e.printStackTrace();
474        }
475    }
476}